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:
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:
use ;
let request = new
.map_body;
let = request.into_parts;
let mut response = new;
response.fields.add?;
assert_eq!;
# Ok::
The pooled client/server example 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 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 for the standard process.
Releases
See releases.md for the full release history.
v0.3.0
base-http1drops 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 reportedmay_have_trailers, which everyBodyReaderdoes, 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 withContent-Length, and a response is sent with its length or close-delimited; trailers either produces are validated and then dropped. This applies toRequestWriter::write_request,RequestWriter::write_frameandResponseWriter::write_response, so it reachesbase-http-clientandbase-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. ATrailerfield, 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 declaresContent-Length: 0instead of being close-delimited. ATrailerfield no longer selects chunked framing on HTTP/1.1 either; framing follows the body, and the field is sent as an ordinary field.base-http1sends an HTTP/1.1 request of known length withContent-Lengtheven when its body may have trailers, and drops any trailers it produces after validating them. Previously such a request, including everyBodyReader::with_lengthupload and any imperative request with aTrailerfield, was chunked. Request trailers are rare, a client cannot assume the server acceptsTransfer-Encoding, and servers that require a length answer a chunked upload with 411. The length comes from the body or from thecontent_lengthgiven toRequestWriter::begin_request, so abase-http-clientrequest opened with acontent_lengthbehaves 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 aBufferedBodyor a receivedContent-Lengthor close-delimited body being forwarded, keepsContent-Length, and HEAD, 204 and 304 responses take no framing.- A body of known length now decides the length a message sends.
base-http1previously refused a request or response whose typedcontent_lengthcontradicted its body's length withLengthMismatch, 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 receivedcontent_length. The typed declaration is now ignored and any generatedContent-Lengthcomes from the body. A response without data, to HEAD or with 304, still uses its declaration, a 204 still refuses one, a rawContent-Lengthfield the body contradicts is still refused, a declared length alongside a rawTransfer-Encodingis still refused, and a declared length on a body of unknown length is still enforced while the body is written.base-httpaddsRequestMetadata::declared_lengthandResponseMetadata::declared_length, which apply the rule and which every codec uses, andRequest::lengthandResponse::length, which return the resulting length for a message that sends data. base-http-serverresponds with500 Internal Server Errorwhen a handler returns a final response that HTTP/1 cannot send, such as an informational status, conflictingContent-LengthandTransfer-Encodingfields, 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 withConnection: close, after any interim responses the handler sent, and then closes the connection. Serving the connection reports the newError::Response, whose source is the refusal, instead ofError::Protocol. A failure after the response has started still only closes the connection. This is a breaking change:Errorgains a public variant.base-http-serverresponds to a handler error with400 Bad Requestor500 Internal Server Error. Previously a handlerErrclosed the connection without writing a response, so the client saw an empty reply. The server now writes a best-effort bodiless response withConnection: 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-http1addsRequestReader::is_poisoned, which reports a failed read. Serving the connection still reportsError::Handler, soservecounts it inStatistics::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-clientchecks 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 withReadableStream::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-http1addsResponseReader::probe_viable, which performs the check.base-http-serverlisteners survive transient accept failures.serveandserve_tlspreviously stopped accepting on any accept error, then joined their connections before closing the listener, so a singleECONNABORTEDorEMFILEleft the port open with nothing accepting. Failures belonging to one pending peer (ECONNABORTED,ECONNRESET,ECONNREFUSED,EINTRincluding WindowsWSAEINTR,EAGAIN,EPROTO, network-down or unreachable errors and, on Linux,EPERMfirewall rejections and the pending-peer errorsENOPROTOOPT,EHOSTDOWNandENONET) are retried immediately, backing off after 16 consecutive failures so a persistent one cannot spin. Resource exhaustion (EMFILE,ENFILE,ENOBUFS,ENOMEM, and WindowsWSAEMFILEandWSAENOBUFS) pauses acceptance for 100 ms on the scheduler's clock, honouring cancellation, while existing connections continue. Both are counted in the newStatistics::accept_errors. Any other accept failure, including scheduler closure,EACCESandEOPNOTSUPP, 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:serveandserve_tlsnow require the scheduler to implementClock, which every Base scheduler backend provides, andStatisticsgains a public field.- Requires
base-scheduler 0.4,base-pool 0.4andbase-endpoint 0.2. The idle-connection check relies on the newReadableStream::try_read_into. This is a breaking change: scheduler and pool types such asCancellationappear in the public API, so callers must use the same versions.
v0.2.0
base-http-clientandbase-http-serverdisable 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 newOptions::tcp_nodelaytofalseto leave the socket options unchanged. Setting the option is best effort and does not fail a connection. This is a breaking change:Optionsin both crates gains the publictcp_nodelayfield, so struct literals need it or..Default::default(), andClient::new,serveandserve_tlsnow require the scheduler to implementSocketSetOption<TcpNoDelay>, which every Base scheduler backend provides.serve_connectionleaves the options of a caller-accepted socket unchanged.base-http1incoming 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-clientandbase-http-server.base-httpdefines 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-http1implements 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 manages101upgrades and CONNECT.base-http-protocolselects the protocol for a connection, including through ALPN.base-http-clientprovides a cloneable client with an explicit driver that pools HTTP/1 connections throughbase-pool, over plain TCP or, with therustlsfeature, TLS.base-http-serverserves connections and listeners with a connection limit, over plain TCP or, with therustlsfeature, 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 isbase_http_client::Error::Connect, which carries the endpoint address. - Requires Rust 1.87,
base-scheduler 0.3,base-pool 0.3andbase-endpoint 0.1.
See Also
base-http1implements native HTTP/1 framing,base-http-protocolselects the protocol for a connection,base-http-clientprovides pooled client calls andbase-http-serverserves applications.- The design documents record package boundaries, the roadmap and implementation evidence, and the performance notes collect benchmark results and reproduction commands.
base-schedulerdefines the runtime-independent scheduler and socket capabilities these crates use.base-poolprovides the connection pool behind the client.base-endpointbinds and connects endpoints through scheduler capabilities, including TLS.
Contributing
Please open an issue or pull request on GitHub.