# 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):

```rust
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):

```rust
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:

```toml
[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):

```rust
tako::serve_with_config(listener, router, config).await;
```

After (2.5):

```rust
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`](https://github.com/rust-dd/tako/tree/main/examples/graphql-generic-ws).

Source: https://tako.rust-dd.com/docs/reference/migration-2-5