Migrating to 2.5
Tako 2.5 replaces tokio-tungstenite with its own WebSocket engine; move handlers from TakoWs to WebSocketUpgrade and compio-ws to compio plus ws.
Tako 2.5.0 ships its own WebSocket engine and drops tokio-tungstenite,
tungstenite and compio-ws, and it removes every API deprecated since 2.1.
WebSocket handlers, deprecated APIs and code that accesses the raw multipart
stream need the changes below. The WebSocket engine and its API are the same on
Tokio and Compio.
Replace TakoWs with WebSocketUpgrade
tako::ws::TakoWs is now tako::ws::WebSocketUpgrade, and the handler receives
a tako::ws::WebSocket instead of a tokio-tungstenite WebSocketStream. Read
with recv(), write with send(), and take Message from tako::ws.
Before (2.4):
use futures_util::SinkExt;
use futures_util::StreamExt;
use tako::responder::Responder;
use tako::types::Request;
use tako::ws::TakoWs;
use tokio_tungstenite::tungstenite::Message;
async fn echo(req: Request) -> impl Responder {
TakoWs::new(req, |mut ws| async move {
while let Some(Ok(msg)) = ws.next().await {
match msg {
Message::Text(_) | Message::Binary(_) => {
if ws.send(msg).await.is_err() {
break;
}
}
Message::Ping(payload) => {
let _ = ws.send(Message::Pong(payload)).await;
}
Message::Close(_) => break,
_ => {}
}
}
})
}After (2.5):
use tako::responder::Responder;
use tako::types::Request;
use tako::ws::WebSocketUpgrade;
async fn echo(req: Request) -> impl Responder {
WebSocketUpgrade::new(req, |mut ws| async move {
while let Some(Ok(msg)) = ws.recv().await {
if (msg.is_text() || msg.is_binary()) && ws.send(msg).await.is_err() {
break;
}
}
})
}Tako answers pings and the peer's close frame itself, so the Ping and Close
arms can go. WebSocket still implements Stream and Sink, so code built on
StreamExt::next, split or forward keeps working once Message comes from
tako::ws.
The message types
2.4 (tungstenite) | 2.5 (tako::ws) |
|---|---|
Message::Text(Utf8Bytes), Message::text(..) | The same, with tako's Utf8Bytes |
Message::Binary(Bytes), Message::binary(..) | The same |
Message::Frame(_) | Removed; raw frames are never yielded |
CloseCode::Normal, CloseCode::from(1000) | CloseCode::NORMAL, CloseCode::new(1000) |
tungstenite::Error | tako::ws::Error: Io, Protocol(ProtocolError) or Closed |
send queues frames
In 2.4 every send wrote its frame before it returned. In 2.5 send queues the
frame, and queued frames go out together when the handler waits for anything
(the next recv(), a timer, a channel), when more than write_buffer_size bytes
are queued (64 KiB by default), or on flush(). An echo loop that receives 16
pipelined messages answers them with one write instead of 16. Call flush()
where the handler must know a frame has left before it continues.
A send from another task, for example through the write half of split(),
writes at once, as before.
Ping and Pong payloads over 125 bytes, close reasons over 123 bytes, and
reserved close codes return Error::Protocol without sending a frame. Payloads
are never silently shortened. Calling close() after receiving the peer's
Close flushes the queued reply; repeated calls only flush.
The connection closes with a close frame
In 2.4 the socket was dropped without a close frame when the handler returned,
panicked, or ran past max_lifetime. In 2.5 the connection closes with code 1000
when the handler returns and no WebSocket handle is left, with 1011 when the
handler panics, and with 1001 when max_lifetime runs out. A WebSocket moved
to another task keeps the connection open until it is dropped.
The driver flushes its Close and shuts down the transport, with a five-second limit for a peer that stops reading. It does not wait for the peer's Close reply; a peer with unread data in flight can observe a transport reset.
Builder methods
keep_alive(WsKeepAlive) never had an effect; it is removed together with
WsKeepAlive. write_buffer_size is new. The other methods keep their names
and arguments: protocols, allowed_origins, max_frame_size,
max_message_size, read_buffer_size, upgrade_timeout and max_lifetime.
read_buffer_size accepts 1 byte through 64 KiB and controls the initial read
allocation, which grows with incoming frame data. Invalid buffer sizes and
limits beyond addressable allocation sizes reject the upgrade with HTTP 500.
Zero frame/message limits permit empty payloads; a zero write threshold flushes
queued output before each send.
Compio
The compio-ws feature and the tako::ws_compio module are gone, together with
TakoWsCompio, CompioWebSocket and UpgradedStream. Enable compio and ws
and use the same tako::ws API as on Tokio:
[dependencies]
tako-rs = { version = "2.5", default-features = false, features = ["compio", "ws"] }Compio's own ws feature is no longer needed.
Removed deprecated APIs
Every item below was deprecated in 2.1 or earlier and is now gone:
| Removed | Use instead |
|---|---|
serve, serve_with_shutdown, serve_with_config, serve_with_shutdown_and_config, and the same four for h2c, tls, h3, unix_http, vsock_http and http_with_proxy_protocol | Server::builder() or CompioServer::builder(), .config(..), then a spawn_* or try_spawn_* method |
tako::state::set_state | Router::with_state, or get_or_init_state for process-global values; it keeps the first value, where set_state replaced it |
Router::state | Router::with_state |
ResponderError | Nothing: any Responder can be the error type of a handler's Result |
set_global_graphql_options | attach_graphql_options on each request, or GraphQLOptions stored with Router::with_state or get_or_init_state |
Before (2.4):
tako::serve_with_config(listener, router, config).await;After (2.5):
let handle = tako::Server::builder()
.config(config)
.build()
.try_spawn_http(listener, router)?;
handle.result().await?;The returned ServerHandle takes over the *_with_shutdown variants:
handle.shutdown(timeout) stops the server, and handle.shutdown_on_signal()
waits for Ctrl-C or SIGTERM first. A MultipartConfig stored with
Router::with_state now configures the multipart extractors, including the disk
spill of BufferedUploadedFile, so code that called set_state(MultipartConfig { .. })
can move to the router. The typed extractor calls the new
FromMultipartField::from_field_with, which hands a custom field type the resolved
config and defaults to from_field.
Other removed public items
These were never produced, did nothing, or duplicated another API:
| Removed | Use instead |
|---|---|
responder::IntoResponse | Responder |
signals::app_events() | signals::app_signals() |
signals::ids::RPC_ERROR | An id of your own; tako never emitted it |
Queue::dead_letters_arc() | Queue::dead_letters(), which now returns Vec<Arc<DeadJob>> |
The bool returned by grpc::grpc_decode | grpc_decode returns the message |
Router::compact_routes | Nothing: it had nothing to remove |
ConnInfo::local, Transport::Tcp | Nothing: neither was ever set |
BodyLimit::new_with_dynamic(limit, f) | BodyLimit::with_dynamic_limit(f); limit never applied |
middleware::timeout on Compio | Route::timeout; the middleware never ran there |
KeyExpansionConfig::key_length and with_key_length | Nothing: derived keys are always 64 bytes |
CookieSigned::verify | CookieSigned::contains |
serve_per_thread_compio, spawn_per_thread_compio | serve_per_thread, spawn_per_thread, which run Compio workers under compio |
server_tls::run and server_tls_compio::run | Server::builder().tls(TlsCert::pem_paths(..)) |
load_certs and load_key under server_tls, server_tls_compio and server_h3 | tako::tls::load_certs and tako::tls::load_key |
tako::server_compio, tako::server_h2c, tako::server_vsock | The server builders; the modules held no public items |
compression::CompressionResponse | Nothing: no API returned it |
server_udp::UdpHandler, webtransport::WebTransportHandler | Nothing: no API took them |
FromRequest for &mut Request | A Request argument; the impl could not be a handler argument |
Route::new | Nothing: it needed a crate-private handler type |
PerThreadShutdown::new and its Default | The handle that spawn_per_thread returns |
The public field GrpcBidi::_phantom | Nothing; it is private |
json_schema::ValidateTarget, tenant::TenantStrategy | JsonSchema::for_request and for_response, and the TenantMiddleware constructors |
Error variants that no code path returned are gone too, so a match arm that
names one no longer compiles and can be deleted: MissingContentType of
JsonError, ProtobufError and SimdJsonError; ParseError of FormError
and QueryError; IpAddrError::{InvalidIpFormat, HeaderParseError};
MultipartError::InvalidContentType;
TypedMultipartError::{InvalidContentType, IoError};
CookiePrivateError::{InvalidKey, DecryptionFailed, InvalidCookieFormat};
CookieSignedError::{InvalidKey, VerificationFailed, InvalidCookieFormat, InvalidSignature};
CookieKeyExpansionError::{InvalidMasterKey, UnsupportedAlgorithm, InvalidKeyLength};
GraphQLError::{MissingQuery, InvalidJson}; and QueueError::UnknownJob.
ClientAuth, ReloadableResolver, H3Congestion, build_rustls_server_config
and the tls module are now re-exported from tako, so the types that
TlsCert and ServerConfig take can be named without depending on
tako-rs-server.
Multipart limits
Untyped multipart extraction now enforces MultipartConfig::max_parts and
allowed_content_types while fields are read. Both TakoMultipart.0 and
TakoMultipart::into_inner() expose MultipartFields instead of raw
multer::Multipart; next_field() returns MultipartError and reports limit
violations as TooManyParts or DisallowedContentType. Code that names the
raw stream or error type must use these wrapper types.
Performance changes
These need no code changes. Against 2.4.2 in the same benchmark run, on a per-thread server with 4 workers echoing 5-byte text messages, 2.5:
- answers 16 pipelined messages per connection with one write instead of 16, serving 8 times as many messages at 512 connections and 7 times at 4,096;
- serves 2 to 19% more messages with one message in flight, from 512 to 16,384 connections, and 3 to 6% more when each connection closes after 10 messages;
- holds slightly less heap per open connection with small messages, about 5.6 KiB against 5.9 KiB.
On Tokio, reads go into uninitialized buffer space. Payloads up to 4 KiB are copied out, and larger ones are handed over without a copy. All 247 cases tako runs from the Autobahn suite pass on both runtimes (its performance and compression groups 9, 12 and 13 are excluded), with no unclean close where 2.4.2 had 127.
Dependencies and GraphQL
Remove tokio-tungstenite from your Cargo.toml, and futures-util if the
socket was its only user.
GraphQLSubscription needs no changes. GraphQLWebSocket now runs over tako's
Message and Error, so a handler that drives it directly hands it the
WebSocket from WebSocketUpgrade; see
examples/graphql-generic-ws.
Last updated on