base-http 0.4.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. Add `base-http-content` to read request bodies into forms declared with [`base-web-content`](https://github.com/base-rs/base-web-content), streaming uploads to storage.

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:release:patch` (or `minor`, `major`, or `bump --version X.Y.Z`), which bumps the version, commits it with a `Release: vVERSION` trailer, validates it and opens the release 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.4.0

- Requires `base-scheduler 0.5`, `base-pool 0.5` and `base-endpoint 0.3`. This is a breaking change: scheduler, pool and endpoint types such as `Cancellation` and `ReadableStream` appear in the public API, so callers must use the same versions. The HTTP crates read sockets only through `read_into` and `try_read_into`, whose readiness contract scheduler 0.5 leaves unchanged, so HTTP/1 input and the client's idle-connection check are unaffected by its wider readiness rule. The Tokio adapter's `read_into` now clears readiness after a short read on Linux and Android, which that contract already allowed. Scheduler 0.5 removes the Direct backend; run on `base-scheduler-native` or `base-scheduler-tokio` instead.
- Add `base-http-content`, which reads a request's body into a structure declared with `base-web-content`'s `#[derive(Content)]`. `read_content` chooses the format from the request's `Content-Type`, refuses a request whose `Content-Length` exceeds the largest body the declaration could accept with 413 before reading any of it, reads the body frame by frame as it arrives, and streams uploads to an `UploadStore`. When the content is refused, or reading or storing fails, it aborts the upload in progress, removes every upload it stored, and closes the body. `ReadError::status` gives one status for each failure: 413, the content error's own 400, 413 or 415, 422 for violations, 400 for a malformed `Content-Type` or a body that fails, and 500 for a store that fails. `FileStore` saves each upload to a new file at a path the application chooses from the field's declared path, never from the submitted filename: it creates the file exclusively, refusing anything at the path including a symbolic link, with permissions 0600 on Unix by default, removes it when the upload is aborted, as `read_content` does whenever writing it fails, and does its file I/O on the scheduler's blocking threads. `NoUploads` reads declarations without uploads. Cleanup is asynchronous, so a read whose future is dropped, as `base-http-server` drops a handler's future at shutdown, leaves what it stored in place. The package depends on `base-http`, `base-web-content`, `base-web-media-type` and the scheduler interface, not on a server or protocol codec.
- `base-http` adds `Status::UNPROCESSABLE_CONTENT` (422).
- `base-http`'s `license.md` and the headers of its messages, fields and bodies, which are ported from `protocol-http`, again credit the authors and years of the files they derive from, restoring the copyright lines that the first release's license update replaced.

### 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".

<!-- 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, [`base-http-server`](https://docs.rs/base-http-server) serves applications and [`base-http-content`](https://docs.rs/base-http-content) reads request content into declared forms.
- 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).