base-http 0.3.0

Runtime-independent HTTP semantics and bodies for Base
Documentation
# `base-http`

A runtime-independent HTTP semantic layer for Base, porting Socketry's `protocol-http` model to Rust. It defines requests, responses, fields and bodies without performing I/O, while sibling packages implement HTTP/1, pooled clients and servers over Base scheduler sockets.

## Usage

Add `base-http` for the semantic types:

```sh
cargo add base-http
```

Add `base-http-client` or `base-http-server` for the HTTP/1 client and server, and enable the `rustls` feature of either for TLS.

Messages carry public metadata and an arbitrary body owner, and body mapping preserves the metadata:

```rust
use base_http::{Method, Request, Response, Status, body::BufferedBody};

let request = Request::new(Method::POST, "/submit", b"hello".to_vec())
    .map_body(BufferedBody::from);
let (metadata, body) = request.into_parts();
let mut response = Response::new(Status::OK, body);
response.fields.add("Content-Type", "text/plain")?;
assert_eq!(metadata.method, Method::POST);
# Ok::<(), base_http::FieldError>(())
```

The [pooled client/server example](crates/client/examples/client_server.rs) sends POST requests through one endpoint pool to a local echo server. Run it with `cargo run --locked --manifest-path crates/client/Cargo.toml --example client_server`, adding `--features rustls` and `-- --https` for TLS. The [low-level example](crates/server/examples/client_server.rs) demonstrates direct ownership of the HTTP/1 lanes.

## Releasing

Prepare a release with `cargo bake cargo:version:patch` (or `minor`, `major`, or `bump --version X.Y.Z`), commit it with a `Release: vVERSION` trailer, then run `cargo bake cargo:release` and open a pull request. After review and merge, GitHub Actions publishes the release when the configured `crates-io` environment approves it, then creates or updates the matching GitHub Release from `releases.md`. Follow the shared [Releasing skill](https://github.com/base-rs/base-project/blob/main/context/releasing.md) for the standard process.

## Releases

<!-- bake-readme:releases:start -->

See [releases.md](releases.md) for the full release history.

### v0.3.0

- `base-http1` drops trailers on HTTP/1.0 instead of refusing them. HTTP/1.0 has no chunked framing, so trailers cannot be carried, and HTTP semantics let a recipient discard them. Previously an HTTP/1.0 request whose body reported `may_have_trailers`, which every `BodyReader` does, was refused with "trailers require HTTP/1.1 chunked framing", and a response to an HTTP/1.0 client failed when its body produced trailers. An HTTP/1.0 request body of known length is now sent with `Content-Length`, and a response is sent with its length or close-delimited; trailers either produces are validated and then dropped. This applies to `RequestWriter::write_request`, `RequestWriter::write_frame` and `ResponseWriter::write_response`, so it reaches `base-http-client` and `base-http-server`. An HTTP/1.0 request body of unknown length is still refused, but now with "HTTP/1.0 request requires a body length", since an HTTP/1.0 request cannot be close-delimited. A `Trailer` field, which is only a hint, no longer causes an HTTP/1.0 request or response to be refused; it is sent as an ordinary field. A 205 response to an HTTP/1.0 client whose body may have trailers now declares `Content-Length: 0` instead of being close-delimited. A `Trailer` field no longer selects chunked framing on HTTP/1.1 either; framing follows the body, and the field is sent as an ordinary field.
- `base-http1` sends an HTTP/1.1 request of known length with `Content-Length` even when its body may have trailers, and drops any trailers it produces after validating them. Previously such a request, including every `BodyReader::with_length` upload and any imperative request with a `Trailer` field, was chunked. Request trailers are rare, a client cannot assume the server accepts `Transfer-Encoding`, and servers that require a length answer a chunked upload with 411. The length comes from the body or from the `content_length` given to `RequestWriter::begin_request`, so a `base-http-client` request opened with a `content_length` behaves the same; writing trailers to it no longer fails. A request of unknown length is still chunked and keeps its trailers. Responses are unchanged: a response body that may have trailers is chunked even when its length is known, while one that cannot have them, such as a `BufferedBody` or a received `Content-Length` or close-delimited body being forwarded, keeps `Content-Length`, and HEAD, 204 and 304 responses take no framing.
- A body of known length now decides the length a message sends. `base-http1` previously refused a request or response whose typed `content_length` contradicted its body's length with `LengthMismatch`, which the server answered with a 500 and the client reported as an error. This usually came from stale metadata, such as a proxy that replaced a body but kept the received `content_length`. The typed declaration is now ignored and any generated `Content-Length` comes from the body. A response without data, to HEAD or with 304, still uses its declaration, a 204 still refuses one, a raw `Content-Length` field the body contradicts is still refused, a declared length alongside a raw `Transfer-Encoding` is still refused, and a declared length on a body of unknown length is still enforced while the body is written. `base-http` adds `RequestMetadata::declared_length` and `ResponseMetadata::declared_length`, which apply the rule and which every codec uses, and `Request::length` and `Response::length`, which return the resulting length for a message that sends data.
- `base-http-server` responds with `500 Internal Server Error` when a handler returns a final response that HTTP/1 cannot send, such as an informational status, conflicting `Content-Length` and `Transfer-Encoding` fields, or an unsolicited upgrade. Previously the connection closed without writing anything, so the client saw an empty reply. Because the writer refuses such a response before writing any of it, the server can still send a best-effort bodiless 500 with `Connection: close`, after any interim responses the handler sent, and then closes the connection. Serving the connection reports the new `Error::Response`, whose source is the refusal, instead of `Error::Protocol`. A failure after the response has started still only closes the connection. This is a breaking change: `Error` gains a public variant.
- `base-http-server` responds to a handler error with `400 Bad Request` or `500 Internal Server Error`. Previously a handler `Err` closed the connection without writing a response, so the client saw an empty reply. The server now writes a best-effort bodiless response with `Connection: close`, after any interim responses the handler sent, then always closes the connection. The status is 400 when reading the request failed, for example because the handler propagated a malformed or truncated body error, and 500 otherwise. `base-http1` adds `RequestReader::is_poisoned`, which reports a failed read. Serving the connection still reports `Error::Handler`, so `serve` counts it in `Statistics::failed_connections`. An error caused by cancellation still closes without a response, and a body failure after the response has started still closes the connection.
- `base-http-client` checks an idle pooled connection before reusing it. A connection the server closed while idle, for example after its keep-alive timeout, was previously handed to the next request, which failed with "truncated HTTP/1 message". Before sending any request bytes on a connection that has already completed an exchange, the client now attempts one immediate read on it with `ReadableStream::try_read_into`, which reaches the transport rather than any readiness the scheduler has cached, so it observes a close the operating system has already received. If the read finds data, EOF or an error, including a TLS close notification, the client retires the connection and the request uses another or a new one. TLS records that carry no application data, such as key updates, keep the connection. A server can still close a connection after the check or during the exchange, so that request still fails, and requests are not retried. `base-http1` adds `ResponseReader::probe_viable`, which performs the check.
- `base-http-server` listeners survive transient accept failures. `serve` and `serve_tls` previously stopped accepting on any accept error, then joined their connections before closing the listener, so a single `ECONNABORTED` or `EMFILE` left the port open with nothing accepting. Failures belonging to one pending peer (`ECONNABORTED`, `ECONNRESET`, `ECONNREFUSED`, `EINTR` including Windows `WSAEINTR`, `EAGAIN`, `EPROTO`, network-down or unreachable errors and, on Linux, `EPERM` firewall rejections and the pending-peer errors `ENOPROTOOPT`, `EHOSTDOWN` and `ENONET`) are retried immediately, backing off after 16 consecutive failures so a persistent one cannot spin. Resource exhaustion (`EMFILE`, `ENFILE`, `ENOBUFS`, `ENOMEM`, and Windows `WSAEMFILE` and `WSAENOBUFS`) pauses acceptance for 100 ms on the scheduler's clock, honouring cancellation, while existing connections continue. Both are counted in the new `Statistics::accept_errors`. Any other accept failure, including scheduler closure, `EACCES` and `EOPNOTSUPP`, or a failed backoff wait, still ends serving with an error, which now takes precedence over a consequent listener close failure. On every shutdown the listener now closes before active connections are cancelled and joined, so new peers are refused immediately. This is a breaking change: `serve` and `serve_tls` now require the scheduler to implement `Clock`, which every Base scheduler backend provides, and `Statistics` gains a public field.
- Requires `base-scheduler 0.4`, `base-pool 0.4` and `base-endpoint 0.2`. The idle-connection check relies on the new `ReadableStream::try_read_into`. This is a breaking change: scheduler and pool types such as `Cancellation` appear in the public API, so callers must use the same versions.

### v0.2.0

- `base-http-client` and `base-http-server` disable Nagle's algorithm (`TCP_NODELAY`) on each TCP connection they establish or accept, so small writes such as streamed frames on long-lived duplex exchanges are not delayed by Nagle's algorithm interacting with delayed acknowledgements. Set the new `Options::tcp_nodelay` to `false` to leave the socket options unchanged. Setting the option is best effort and does not fail a connection. This is a breaking change: `Options` in both crates gains the public `tcp_nodelay` field, so struct literals need it or `..Default::default()`, and `Client::new`, `serve` and `serve_tls` now require the scheduler to implement `SocketSetOption<TcpNoDelay>`, which every Base scheduler backend provides. `serve_connection` leaves the options of a caller-accepted socket unchanged.
- `base-http1` incoming body reads are cancellation safe. Dropping a pending read, or cancelling its wait, loses no input, so a later read resumes at the same position. Previously any later read failed with "previous body read failed or was abandoned". A read that fails for another reason still fails every later read, now with "previous body read failed".

### v0.1.0

- First release of `base-http`, `base-http1`, `base-http-protocol`, `base-http-client` and `base-http-server`. `base-http` defines runtime-independent HTTP semantics: requests and responses, methods, statuses, versions, request targets, ordered fields with header interpretation and combination policies, interim responses, and buffered and streaming bodies. Constructing messages and mapping bodies performs no I/O.
- `base-http1` implements native HTTP/1.0 and HTTP/1.1 framing over Base scheduler streams. It parses heads with a hand-written parser and handles fixed-length, chunked and close-delimited bodies, trailers and interim responses. The server side also manages `101` upgrades and CONNECT. `base-http-protocol` selects the protocol for a connection, including through ALPN.
- `base-http-client` provides a cloneable client with an explicit driver that pools HTTP/1 connections through `base-pool`, over plain TCP or, with the `rustls` feature, TLS. `base-http-server` serves connections and listeners with a connection limit, over plain TCP or, with the `rustls` feature, TLS.
- Errors report each cause once: an error either describes its cause in its message or returns it from `source`. A connection that fails to open is `base_http_client::Error::Connect`, which carries the endpoint address.
- Requires Rust 1.87, `base-scheduler 0.3`, `base-pool 0.3` and `base-endpoint 0.1`.

<!-- bake-readme:releases:end -->

## See Also

- [`base-http1`](https://docs.rs/base-http1) implements native HTTP/1 framing, [`base-http-protocol`](https://docs.rs/base-http-protocol) selects the protocol for a connection, [`base-http-client`](https://docs.rs/base-http-client) provides pooled client calls and [`base-http-server`](https://docs.rs/base-http-server) serves applications.
- The [design documents](design/) record package boundaries, the roadmap and implementation evidence, and the [performance notes](performance.md) collect benchmark results and reproduction commands.
- [`base-scheduler`](https://docs.rs/base-scheduler) defines the runtime-independent scheduler and socket capabilities these crates use.
- [`base-pool`](https://docs.rs/base-pool) provides the connection pool behind the client.
- [`base-endpoint`](https://docs.rs/base-endpoint) binds and connects endpoints through scheduler capabilities, including TLS.

## Contributing

Please open an issue or pull request on [GitHub](https://github.com/base-rs/base-http).