Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2f88793
transfer-encoding parsing robustness
duke8253 Jan 27, 2026
6e4b801
Avoid close delimit mode on http/1.0 req
drcaramelsyrup Jan 31, 2026
2142225
Upgrade body mode on subrequest
drcaramelsyrup Feb 1, 2026
ed7445e
Reject invalid content-length v1 requests
drcaramelsyrup Feb 1, 2026
f980c9c
Add the ability limit the number of times an upstream connection can …
johnhurt Feb 9, 2026
d890295
Add a builder for pingora proxy service to avoid parameter combinatio…
johnhurt Feb 10, 2026
64c486a
Add is_upgrade_req() and was_upgraded() to custom server Session trait
Feb 11, 2026
0869a84
Reject non-chunked transfer-encoding
drcaramelsyrup Feb 11, 2026
f14a325
Disable CONNECT method proxying by default
drcaramelsyrup Feb 13, 2026
992a8a6
Correct the custom protocol code for shutdown
Feb 18, 2026
8ee1122
Handle custom ALPNs in s2n impl of ALPN::to_wire_protocols() (#1)
varunravi98 Feb 17, 2026
151379e
Add a system for specifying and using service-level dependencies
johnhurt Feb 19, 2026
22d2310
remove `CacheKey::default` impl
gumpt Feb 19, 2026
740cb4f
Check fd match in h2 connector
drcaramelsyrup Feb 14, 2026
9d90d69
Fix upgrade handling if body not init
drcaramelsyrup Feb 19, 2026
5e93e46
Validate invalid content-length on v1 resp by default
drcaramelsyrup Feb 1, 2026
2bf5455
fix(windows): add missing `RawStream::Virtual` match arm in `AsRawSoc…
soddygo Feb 9, 2026
739dee2
Add Windows support and block until main loop returns
danechitoaie Dec 4, 2025
04e1b20
fix invalid CL option on ALPN fallbacks
drcaramelsyrup Feb 20, 2026
82f1404
Recognize dcb/dcz in compression handling
schunduri-blip Jan 30, 2026
53b2f4b
fix: don't use all for socket
justinrubek Feb 11, 2026
8f1db06
don't consider "bytes=" a valid range header
gumpt Feb 26, 2026
522f327
strip {content, transfer}-encoding from 416s
gumpt Feb 26, 2026
ec197dc
Allow server bootstrapping to take place in the context of services w…
johnhurt Feb 26, 2026
89c92fa
Bump pingora to version 0.8.0
johnhurt Feb 27, 2026
96bba36
Update changelog for 0.8.0
johnhurt Mar 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bleep
Original file line number Diff line number Diff line change
@@ -1 +1 @@
71f26703aeb326cc03ccf2d200a1784c915ffb49
d2c25d726c5738e6a8028dc3e7642ecfe6c1824e
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,40 @@

All notable changes to this project will be documented in this file.

## [0.8.0](https://github.com/cloudflare/pingora/compare/0.7.0...0.8.0) - 2026-03-02


**🚀 Features**

* Add support for client certificate verification in mTLS configuration.
* Add upstream\_write\_pending\_time to Session for upload diagnostics.
* Pipe subrequests utility: creates a state machine to treat subrequests as a "pipe," enabling direct sending of request body and writing of response tasks, with a handler for error propagation and support for reusing a preset or captured input body for chained subrequests.
* Add the ability to limit the number of times a downstream connection can be reused
* Add a system for specifying and using service-level dependencies
* Add a builder for pingora proxy service, e.g. to specify ServerOptions.

**🐛 Bug Fixes**

* Fix various Windows compiler issues.
* Handle custom ALPNs in s2n impl of ALPN::to\_wire\_protocols() to fix s2n compile issues.
* Fix: don't use “all” permissions for socket.
* Fix a bug with the ketama load balancing where configurations were not persisted after updates.
* Ensure http1 downstream session is not reused on more body bytes than expected.
* Send RST\_STREAM CANCEL on application read timeouts for h2 client.
* Start close-delimited body mode after 101 is received for WebSocket upgrades. `UpgradedBody` is now an explicit HttpTask.
* Avoid close delimit mode on http/1.0 req.
* Reject invalid content-length http/1 requests to eliminate ambiguous request framing.
* Validate invalid content-length on http/1 resp by default, and removes content-length from the response if transfer-encoding is present, per RFC.
* Correct the custom protocol code for shutdown: changed the numeric code passed on shutdown to 0 to indicate an explicit shutdown rather than a transport error.

**⚙️ Miscellaneous Tasks**

* Remove `CacheKey::default` impl, users of caching should implement `cache_key_callback` themselves
* Allow server bootstrapping to take place in the context of services with dependents and dependencies
* Don't consider "bytes=" a valid range header: added an early check for an empty/whitespace-only range-set after the `bytes=` prefix, returning 416 Range Not Satisfiable, consistent with RFC 9110 14.1.2.
* Strip {content, transfer}-encoding from 416s to mirror the behavior for 304 Not Modified responses.
* Disable CONNECT method proxying by default, with an option to enable via server options; unsupported requests will now be automatically rejected.

## [0.7.0](https://github.com/cloudflare/pingora/compare/0.6.0...0.7.0) - 2026-01-30

### Highlights
Expand Down
2 changes: 1 addition & 1 deletion pingora-boringssl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pingora-boringssl"
version = "0.7.0"
version = "0.8.0"
authors = ["Yuchen Wu <yuchen@cloudflare.com>"]
license = "Apache-2.0"
edition = "2021"
Expand Down
14 changes: 7 additions & 7 deletions pingora-cache/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pingora-cache"
version = "0.7.0"
version = "0.8.0"
authors = ["Yuchen Wu <yuchen@cloudflare.com>"]
license = "Apache-2.0"
edition = "2021"
Expand All @@ -18,12 +18,12 @@ name = "pingora_cache"
path = "src/lib.rs"

[dependencies]
pingora-core = { version = "0.7.0", path = "../pingora-core", default-features = false }
pingora-error = { version = "0.7.0", path = "../pingora-error" }
pingora-header-serde = { version = "0.7.0", path = "../pingora-header-serde" }
pingora-http = { version = "0.7.0", path = "../pingora-http" }
pingora-lru = { version = "0.7.0", path = "../pingora-lru" }
pingora-timeout = { version = "0.7.0", path = "../pingora-timeout" }
pingora-core = { version = "0.8.0", path = "../pingora-core", default-features = false }
pingora-error = { version = "0.8.0", path = "../pingora-error" }
pingora-header-serde = { version = "0.8.0", path = "../pingora-header-serde" }
pingora-http = { version = "0.8.0", path = "../pingora-http" }
pingora-lru = { version = "0.8.0", path = "../pingora-lru" }
pingora-timeout = { version = "0.8.0", path = "../pingora-timeout" }
bstr = { workspace = true }
http = { workspace = true }
indexmap = "1"
Expand Down
14 changes: 0 additions & 14 deletions pingora-cache/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@

//! Cache key

use super::*;

use blake2::{Blake2b, Digest};
use http::Extensions;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -214,18 +212,6 @@ impl CacheKey {
hasher
}

/// Create a default [CacheKey] from a request, which just takes its URI as the primary key.
pub fn default(req_header: &ReqHeader) -> Self {
CacheKey {
namespace: Vec::new(),
primary: format!("{}", req_header.uri).into_bytes(),
primary_bin_override: None,
variance: None,
user_tag: "".into(),
extensions: Extensions::new(),
}
}

/// Create a new [CacheKey] from the given namespace, primary, and user_tag input.
///
/// Both `namespace` and `primary` will be used for the primary hash
Expand Down
21 changes: 11 additions & 10 deletions pingora-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pingora-core"
version = "0.7.0"
version = "0.8.0"
authors = ["Yuchen Wu <yuchen@cloudflare.com>"]
license = "Apache-2.0"
edition = "2021"
Expand All @@ -19,15 +19,15 @@ name = "pingora_core"
path = "src/lib.rs"

[dependencies]
pingora-runtime = { version = "0.7.0", path = "../pingora-runtime" }
pingora-openssl = { version = "0.7.0", path = "../pingora-openssl", optional = true }
pingora-boringssl = { version = "0.7.0", path = "../pingora-boringssl", optional = true }
pingora-pool = { version = "0.7.0", path = "../pingora-pool" }
pingora-error = { version = "0.7.0", path = "../pingora-error" }
pingora-timeout = { version = "0.7.0", path = "../pingora-timeout" }
pingora-http = { version = "0.7.0", path = "../pingora-http" }
pingora-rustls = { version = "0.7.0", path = "../pingora-rustls", optional = true }
pingora-s2n = { version = "0.7.0", path = "../pingora-s2n", optional = true }
pingora-runtime = { version = "0.8.0", path = "../pingora-runtime" }
pingora-openssl = { version = "0.8.0", path = "../pingora-openssl", optional = true }
pingora-boringssl = { version = "0.8.0", path = "../pingora-boringssl", optional = true }
pingora-pool = { version = "0.8.0", path = "../pingora-pool" }
pingora-error = { version = "0.8.0", path = "../pingora-error" }
pingora-timeout = { version = "0.8.0", path = "../pingora-timeout" }
pingora-http = { version = "0.8.0", path = "../pingora-http" }
pingora-rustls = { version = "0.8.0", path = "../pingora-rustls", optional = true }
pingora-s2n = { version = "0.8.0", path = "../pingora-s2n", optional = true }
bstr = { workspace = true }
tokio = { workspace = true, features = ["net", "rt-multi-thread", "signal"] }
tokio-stream = { workspace = true }
Expand Down Expand Up @@ -72,6 +72,7 @@ httpdate = "1"
x509-parser = { version = "0.16.0", optional = true }
ouroboros = { version = "0.18.4", optional = true }
lru = { workspace = true, optional = true }
daggy = "0.8"

[target.'cfg(unix)'.dependencies]
daemonize = "0.5.0"
Expand Down
102 changes: 102 additions & 0 deletions pingora-core/examples/bootstrap_as_a_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2026 Cloudflare, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Example demonstrating how to start a server using [`Server::bootstrap_as_a_service`]
//! instead of calling [`Server::bootstrap`] directly.
//!
//! # Why `bootstrap_as_a_service`?
//!
//! [`Server::bootstrap`] runs the bootstrap phase synchronously before any services start.
//! This means the calling thread blocks during socket FD acquisition and Sentry initialization.
//!
//! [`Server::bootstrap_as_a_service`] instead schedules bootstrap as a dependency-aware init
//! service. This allows other services to declare a dependency on the bootstrap handle and
//! ensures they only start after bootstrap completes — while keeping setup fully asynchronous
//! and composable with the rest of the service graph.
//!
//! Use `bootstrap_as_a_service` when:
//! - You want to integrate bootstrap into the service dependency graph
//! - You want services to wait for bootstrap without blocking the main thread
//! - You are building more complex startup sequences (e.g. multiple ordered init steps)
//!
//! # Running the example
//!
//! ```bash
//! cargo run --example bootstrap_as_a_service --package pingora-core
//! ```
//!
//! # Expected behaviour
//!
//! Bootstrap runs as a service before `MyService` starts. `MyService` declares a dependency
//! on the bootstrap handle, so it will not be started until bootstrap has completed.

use async_trait::async_trait;
use log::info;
use pingora_core::server::configuration::Opt;
#[cfg(unix)]
use pingora_core::server::ListenFds;
use pingora_core::server::{Server, ShutdownWatch};
use pingora_core::services::Service;

/// A simple application service that requires bootstrap to be complete before it starts.
pub struct MyService;

#[async_trait]
impl Service for MyService {
async fn start_service(
&mut self,
#[cfg(unix)] _fds: Option<ListenFds>,
mut shutdown: ShutdownWatch,
_listeners_per_fd: usize,
) {
info!("MyService: bootstrap is complete, starting up");

// Keep running until a shutdown signal is received.
shutdown.changed().await.ok();

info!("MyService: shutting down");
}

fn name(&self) -> &str {
"my_service"
}

fn threads(&self) -> Option<usize> {
Some(1)
}
}

fn main() {
env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Info)
.init();

let opt = Opt::parse_args();
let mut server = Server::new(Some(opt)).unwrap();

// Schedule bootstrap as a service instead of calling server.bootstrap() directly.
// The returned handle can be used to declare dependencies so that other services
// only start after bootstrap has finished.
let bootstrap_handle = server.bootstrap_as_a_service();

// Register our application service and get its handle.
let service_handle = server.add_service(MyService);

// MyService will not start until the bootstrap service has signaled that it is ready.
service_handle.add_dependency(&bootstrap_handle);

info!("Starting server — bootstrap will run as a service before MyService starts");

server.run_forever();
}
Loading