Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
066fd53
feat(core): restore client parity baseline
quinnj Jan 25, 2026
2b3e11e
feat(client): add HTTP.open streaming support
quinnj Jan 25, 2026
b7464e9
feat(server): add listen and stream handling
quinnj Jan 25, 2026
42bd2f2
feat(websockets): handle close and fragmentation
quinnj Jan 25, 2026
97ce1bb
feat(retry): align retry semantics with v1
quinnj Jan 25, 2026
bd3be6f
feat(trailers): support trailing headers
quinnj Jan 25, 2026
eb35783
feat(http2): add stream manager support
quinnj Jan 25, 2026
b10590c
feat(http2): add settings/ping/goaway helpers
quinnj Jan 25, 2026
89fff76
feat(metrics): expose http manager metrics
quinnj Jan 25, 2026
4f1a32f
fix(websockets): return close error on closed receive
quinnj Jan 25, 2026
f752564
docs: document http2, trailers, and metrics
quinnj Jan 25, 2026
db9b9f6
feat(http2): add server push promise support
quinnj Jan 25, 2026
ff03d01
feat(metrics): add connection monitoring hooks
quinnj Jan 25, 2026
9b8075a
fix(server): use server TLS options
quinnj Jan 25, 2026
69a374a
feat(client): stream IO request bodies
quinnj Jan 25, 2026
1906ec0
feat(client): add observelayers context
quinnj Jan 25, 2026
a515f4e
feat(api): export stream helpers
quinnj Jan 25, 2026
76b298b
feat(stream): add readall, closebody, isaborted
quinnj Jan 25, 2026
ae1d611
feat(util): add download helper
quinnj Jan 25, 2026
90da927
feat(api): add nobody constant
quinnj Jan 25, 2026
27735c1
fix(api): add escape deprecation alias
quinnj Jan 25, 2026
88a6b8c
feat(websockets): add upgrade/listen parity
quinnj Jan 25, 2026
24d3084
docs: update websockets and migration guides
quinnj Jan 25, 2026
23b43fe
feat(api): add utility parity helpers
quinnj Jan 25, 2026
0002302
feat(api): add HTTP error types and trailers
quinnj Jan 25, 2026
ae2ee9d
feat(client): handle iterable chunked bodies
quinnj Jan 25, 2026
0bf5d04
fix(client): fail on invalid body streams
quinnj Jan 25, 2026
bb44362
feat(metrics): track request body length
quinnj Jan 25, 2026
3915923
feat(api): export header helpers
quinnj Jan 25, 2026
932dfa8
test(client): avoid duplicate headers
quinnj Jan 25, 2026
24eb4d5
feat(client): add http2 stream manager options
quinnj Jan 25, 2026
b2085d2
docs(migrate): update v2 migration notes
quinnj Jan 25, 2026
200d94d
feat(server): stream http2 responses
quinnj Jan 25, 2026
b445bdd
feat(client): add proxy basic auth
quinnj Jan 25, 2026
72afcb9
feat(access-log): parse basic auth remote_user
quinnj Jan 25, 2026
9617630
feat(websockets): enforce frame and fragment limits
quinnj Jan 25, 2026
161c9e0
feat(retry): integrate aws retry strategy partition
quinnj Jan 25, 2026
57598b5
feat(access-log): track streamed response bytes
quinnj Jan 25, 2026
6726a44
fix(retry): use aws strategy only with partition
quinnj Jan 25, 2026
10d9b86
feat(http2): add initial settings options
quinnj Jan 25, 2026
d9a0b8a
docs(migrate): align with current v2 behavior
quinnj Jan 26, 2026
353364b
feat(http2): add max closed streams option
quinnj Jan 26, 2026
518ecc8
feat(http2): add initial window size option
quinnj Jan 26, 2026
a9d780e
feat(http2): add manual window controls
quinnj Jan 26, 2026
eddc1b2
fix(http2): validate window sizes
quinnj Jan 26, 2026
6260637
Switch to pure Julia AwsIO/AwsHTTP packages
quinnj Feb 2, 2026
bbec902
Avoid external redirect target in client tests
quinnj Feb 2, 2026
4ccfcd7
Select server handler from TLS protocol
quinnj Feb 2, 2026
215b77a
Restore headers vector semantics
quinnj Feb 3, 2026
6636cec
Restore pool compatibility
quinnj Feb 3, 2026
6fe2fa4
Restore server shutdown hooks
quinnj Feb 3, 2026
3b23a2c
Avoid closing shared connections on readtimeout
quinnj Feb 3, 2026
c9bbad9
Align WebSocket upgrade and protocol handling
quinnj Feb 3, 2026
d15180a
Align HTTP/2 host and authority handling
quinnj Feb 3, 2026
eda7c60
Add HTTP/2 readtimeout handling
quinnj Feb 3, 2026
5fa2255
Remove ErrorResult, adopt exception-based error model
quinnj Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
/test/websockets/reports/*
.idea/*
.vscode
!test/fixtures/http2.key
!test/fixtures/http2.crt
14 changes: 6 additions & 8 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,27 @@ Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
LibAwsCommon = "c6e421ba-b5f8-4792-a1c4-42948de3ed9d"
LibAwsHTTPFork = "d3f1d20b-921e-4930-8491-471e0be3121a"
LibAwsIO = "a5388770-19df-4151-b103-3d71de896ddf"
AwsHTTP = "d4eb1443-154a-48c0-b55a-2f1d1087a5c5"
Reseau = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"

[compat]
CodecZlib = "0.7"
JSON = "0.21.4, 1"
LibAwsCommon = "1.3"
LibAwsHTTPFork = "1.0.2"
LibAwsIO = "1.2.0"
AwsHTTP = "0.1"
Reseau = "1.1"
PrecompileTools = "1.2.1"
URIs = "1"
julia = "1.10"

[extras]
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["JSON", "Test"]
test = ["JSON", "Sockets", "Test"]
92 changes: 89 additions & 3 deletions docs/src/manual/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,14 @@ The following keyword arguments (which correspond to the non-`scheme`/`host`/`po
- **chunkedbody**: To send a request body in chunks, an iterable must be provided where each element is one of the valid types of request bodies mentioned above.
- **modifier**: A function of the form `f(request, body) -> newbody`, i.e. that takes the HTTP request object and proposed request body, and can optionally return a new request body. If the modifer only modifies the request object, it should return `nothing`, which will ensure the original request body is sent unmodified.
-- Response options:
- **response_body**: By default, response bodies are returned as `Vector{UInt8}`. Alternatively, a preallocated `AbstractVector{UInt8}` or any `IO` object can be provided for the response body to be written into.
- **response_body**: By default, response bodies are returned as `Vector{UInt8}`. Alternatively, a preallocated `AbstractVector{UInt8}` or any `IO` object can be provided for the response body to be written into. `response_stream` is a compatible alias.
- **decompress**: If `true`, the response body will be decompressed if it is compressed. By default, response bodies with the `Content-Encoding: gzip` header are decompressed.
- **status_exception**: Default `true`. If `true`, an exception will be thrown if the response status code is not in the 200-299 range.
- **readtimeout**: The maximum time in seconds to wait for a response from the server. Only valid for HTTP/1.1 connections.
-- Retry options (per request):
- **retry_non_idempotent**: Default `false`. If `true`, non-idempotent requests may be retried.
- **retry_check**: Optional function to override retry decisions. It is called as `retry_check(delay, err, req, resp, resp_body)` when a retry is being considered.
- **retry_delays**: Custom retry delays. Provide a number (seconds) or any iterator of delays; defaults to exponential backoff.
-- Redirect options:
- **redirect**: Default `true`. If `true`, the client will follow redirects.
- **redirect_limit**: The maximum number of redirects to follow. Default is 3.
Expand Down Expand Up @@ -77,8 +81,11 @@ The following keyword arguments (which correspond to the non-`scheme`/`host`/`po
- **proxy_ssl_cacert**: The path to the CA certificate file for the proxy.
- **proxy_ssl_insecure**: Default `false`. If `true`, SSL certificate verification will be disabled for the proxy.
- **proxy_ssl_alpn_list**: A list of ALPN protocols to use for the proxy connection. Default is `"h2;http/1.1"`.
- **proxy_auth**: Optional. Set to `:basic` to enable basic auth on proxy requests.
- **proxy_username**: Username for proxy basic auth (requires explicit `proxy_host`/`proxy_port`).
- **proxy_password**: Password for proxy basic auth (requires explicit `proxy_host`/`proxy_port`).
-- Retry options:
- **max_retries**: The maximum number of times to retry a request. Default is 10.
- **max_retries**: The maximum number of times to retry a request. Default is 4.
- **retry_partition**: Requests utilizing the same retry partition (an arbitrary string) will coordinate retries against each other to not overwhelm a temporarily unresponsive server.
- **backoff_scale_factor_ms**: The factor by which to scale the backoff time between retries. Default is 25.
- **max_backoff_secs**: The maximum time in seconds to wait between retries. Default is 20.
Expand All @@ -88,6 +95,25 @@ The following keyword arguments (which correspond to the non-`scheme`/`host`/`po
-- Connection pool options:
- **max_connections**: The maximum number of connections to keep open in the connection pool. Default is 512.
- **max_connection_idle_in_milliseconds**: The maximum time in milliseconds to keep a connection open in the pool. Default is 60000.
- **connection_acquisition_timeout_ms**: The maximum time in milliseconds to wait for a connection from the pool. Default is 0 (no timeout).
- **max_pending_connection_acquisitions**: Maximum number of pending connection acquisitions. Default is 0 (no limit).
- **enable_read_back_pressure**: If `true`, enable back pressure on reads to limit buffered data. Default `false`.
- **response_first_byte_timeout_ms**: Maximum time in milliseconds to wait for the first response byte. Default is 0 (disabled). For per-request control, use `readtimeout`.
-- Monitoring options:
- **monitoring_minimum_throughput_bytes_per_second**: Minimum throughput to consider the connection healthy. Default is 0 (disabled).
- **monitoring_allowable_throughput_failure_interval_seconds**: Seconds of below-minimum throughput before the connection is closed. Default is 0 (disabled).
- **monitoring_statistics_observer**: Optional callback `(connection_nonce, stats) -> nothing` invoked with connection stats samples.
-- HTTP/2 options:
- **http2_prior_knowledge**: Default `false`. If `true`, assume HTTP/2 without ALPN negotiation.
- **http2_stream_manager**: Default `false`. If `true`, enable the HTTP/2 stream manager for multiplexed requests.
- **http2_close_connection_on_server_error**: Default `false`. If `true`, close HTTP/2 connections when a 5xx response is received.
- **http2_connection_manual_window_management**: Default `false`. If `true`, use manual connection-level flow control (call `HTTP.http2_update_window`).
- **http2_connection_ping_period_ms**: Default `0` (disabled). Period in milliseconds for sending HTTP/2 PING frames.
- **http2_connection_ping_timeout_ms**: Default `0` (AWS default). Timeout in milliseconds for PING responses.
- **http2_ideal_concurrent_streams_per_connection**: Default `0` (AWS default). Target streams per connection before opening new connections.
- **http2_max_concurrent_streams_per_connection**: Default `0` (no explicit limit). Upper bound for streams per connection.
- **http2_max_closed_streams**: Default `0` (AWS default). Max closed streams to remember before ignoring late frames.
- **http2_initial_window_size**: Default `65535`. Initial flow-control window size for HTTP/2 (must be <= 2^31-1).
-- AWS runtime options:
- **allocator**: The allocator to use for AWS-allocated memory during the request.
- **bootstrap**: The AWS client bootstrap to use for the request.
Expand Down Expand Up @@ -158,6 +184,67 @@ response = HTTP.get("https://api.example.com/data"; client = custom_client)
println(String(response.body))
\`\`\`

## HTTP/2 Features (Advanced)

### Stream Manager

For high-concurrency HTTP/2 workloads, you can enable the HTTP/2 stream manager. This allows multiple in-flight
streams to share pooled HTTP/2 connections.

\`\`\`julia
using HTTP

client = HTTP.Client("https", "example.com", 443; http2_stream_manager=true)
resp = HTTP.get("https://example.com/resource"; client=client)
\`\`\`

### Connection Control Helpers

When a connection negotiates HTTP/2, you can use the following helpers:

- `HTTP.http2_ping(client; data=nothing)` -> returns round-trip time in nanoseconds.
- `HTTP.http2_change_settings(client, settings)` where `settings` is a vector of pairs or `aws_http2_setting`.
- `HTTP.http2_local_settings(client)` / `HTTP.http2_remote_settings(client)` -> returns current settings.
- `HTTP.http2_send_goaway(client, error_code; allow_more_streams=true, debug_data=nothing)`
- `HTTP.http2_get_sent_goaway(client)` / `HTTP.http2_get_received_goaway(client)` -> returns `nothing` if no GOAWAY.
- `HTTP.http2_update_window(client_or_stream, increment)` -> increases the HTTP/2 connection flow-control window.
- `HTTP.update_window(stream, increment)` -> increases the stream flow-control window (useful with `enable_read_back_pressure=true`).

HTTP/2-specific helpers require an HTTP/2 connection and will throw an `ArgumentError` if the connection is HTTP/1.1.

## Trailing Headers

Trailing headers are available on responses as `resp.trailers` after the response completes. For streaming requests,
you can attach trailers before closing the write side of the stream.

\`\`\`julia
using HTTP

resp = HTTP.open("POST", "https://example.com/upload") do stream
write(stream, "chunk-1")
write(stream, "chunk-2")
HTTP.addtrailer(stream, "x-checksum" => "abc123")
end

resp.trailers === nothing || println(HTTP.header(resp.trailers, "x-server-checksum"))
\`\`\`

## Metrics and Observability

Each response includes a `metrics` field:

- `response.metrics.request_body_length`
- `response.metrics.response_body_length`
- `response.metrics.nretries`
- `response.metrics.stream_metrics` (AWS CRT `aws_http_stream_metrics`)

For connection-level metrics, use `HTTP.manager_metrics(client)`, which returns `aws_http_manager_metrics` with
`available_concurrency`, `pending_concurrency_acquires`, and `leased_concurrency`.

To enable periodic connection statistics callbacks, pass `monitoring_statistics_observer` in `ClientSettings`. The
callback receives a `connection_nonce` and a vector of stats entries. Each entry has a `category` field
(`:http1_channel` or `:http2_channel`) and category-specific fields like `pending_outgoing_stream_ms`.

## Under the Hood (Advanced)

When you call `HTTP.request`, the following advanced steps occur:
Expand All @@ -179,4 +266,3 @@ When you call `HTTP.request`, the following advanced steps occur:

6. **Response Processing:**
The response is parsed, and if errors occur (as dictated by your settings), an exception is raised.

35 changes: 23 additions & 12 deletions docs/src/manual/migrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ r = HTTP.request("GET", "http://example.com")
# Accessing fields
status = r.status
body_text = String(r.body) # Similar to v1.x
header_value = HTTP.header(r.headers, "Content-Type")
header_value = HTTP.header(r, "Content-Type")
# or HTTP.header(r.headers, "Content-Type")
```

Key differences:
- Headers access has changed slightly to operate on the `headers` field
- The `.body` field can now be any type, not just `Vector{UInt8}`
- Headers access works directly on `Request`/`Response`, or on the `headers` field if you already have it
- Request/response *body arguments* accept strings, byte vectors, Dict/NamedTuple (form-encoded), `HTTP.Form`, or IO
- `Response.body` remains a `Vector{UInt8}` for buffered responses (or `nothing` when streaming into `response_body`)
- Context dictionary access is now through `.context` rather than request-specific fields

### Making Requests
Expand All @@ -51,8 +53,10 @@ While the basic request syntax remains similar, there are some changes to keywor

#### Changes to Keyword Arguments

- The `response_stream` keyword argument is still supported, but HTTP.jl no longer automatically closes this stream when done - you need to handle this yourself
- The preferred keyword for streaming responses is `response_body`. `response_stream` is still supported for compatibility
- Response streams are not automatically closed; you need to handle this yourself
- `retry` behavior has been overhauled with more consistent rules for what is retryable
- The default `max_retries` is now 4 (was 10 in v1.x)
- Some connection-related options have new defaults (e.g., TLS is now OpenSSL-based by default rather than MbedTLS)

Example:
Expand Down Expand Up @@ -84,7 +88,7 @@ end

# After (v2.0)
HTTP.open("GET", "http://example.com") do http
# Start reading must be explicitly called
# Optional: call startread to access headers before reading the body
startread(http)
while !eof(http)
data = readavailable(http)
Expand Down Expand Up @@ -133,7 +137,7 @@ end
```

Key differences:
- Most server functionality has been standardized around `serve`/`serve!` rather than `listen`/`listen!`
- `serve`/`serve!` are the primary entry points; `listen`/`listen!` remain available for stream handlers and WebSockets
- The handler typically works with `Request`/`Response` objects rather than `Stream` objects
- The lifecycle management for servers has improved with clearer semantics for `isopen`, `close`, and `wait`

Expand Down Expand Up @@ -166,6 +170,7 @@ end
```

Note the addition of the `stream=true` keyword argument to indicate you want to work with a stream handler.
You can also use `HTTP.listen`/`HTTP.listen!` as shorthand for `stream=true`.

### Router and Middleware

Expand Down Expand Up @@ -270,7 +275,7 @@ close(server)
using HTTP.WebSockets

# Non-blocking server
server = WebSockets.serve!("127.0.0.1", 8081) do ws
server = WebSockets.listen!("127.0.0.1", 8081) do ws
for msg in ws
# Echo back any received message
send(ws, msg)
Expand All @@ -281,7 +286,8 @@ end
close(server)
```

Note the change from `listen!` to `serve!` to maintain consistency with the HTTP server API.
`serve`/`serve!` are the primary request/response handlers. `listen`/`listen!` are stream-handler shorthands
equivalent to `serve(...; stream=true)`.

## Error Handling

Expand All @@ -302,7 +308,7 @@ catch e
if e isa HTTP.ConnectError
println("Connection failed: $(e.error)")
elseif e isa HTTP.TimeoutError
println("Request timed out after $(e.timeout) seconds")
println("Request timed out after $(e.readtimeout) seconds")
elseif e isa HTTP.StatusError
println("Server returned error status: $(e.status)")
elseif e isa HTTP.RequestError
Expand All @@ -325,16 +331,21 @@ jar = HTTP.CookieJar()
response = HTTP.get("https://example.com", cookiejar=jar)

# Checking cookies
cookies = HTTP.getcookies(jar, "example.com")
cookies = HTTP.Cookies.getcookies!(jar, "https", "example.com", "/")
```

## Other Notable Changes

- **URI Handling**: URIs are now handled by the separate URIs.jl package (this change actually occurred in v1.0)
- **Default Headers**: Headers like `Accept: */*` are now included by default in requests
- **TLS Implementation**: OpenSSL is now the default TLS provider instead of MbedTLS
- **TLS Implementation**: TLS is handled by AWS CRT (s2n-tls) instead of MbedTLS
- **Multithreading**: Improved thread safety throughout the codebase
- **Performance**: Significant performance improvements, especially for high-throughput servers
- **Parser APIs**: Low-level parser APIs from v1.x have been removed in v2.0
- **Trailing Headers**: Trailing headers are now captured on `Request.trailers` and `Response.trailers`, and can be sent with `HTTP.addtrailer` when streaming.
- **HTTP/2 Controls**: HTTP/2 helpers (ping, settings, GOAWAY) are available for advanced connection management.
- **Metrics**: Responses include `response.metrics` and clients expose `HTTP.manager_metrics` for connection manager stats.
- **Monitoring**: Optional connection monitoring callbacks are available via `monitoring_statistics_observer`.

## Transitioning Tips

Expand All @@ -350,4 +361,4 @@ cookies = HTTP.getcookies(jar, "example.com")
- Custom client-side layers from v1.x are not compatible with v2.0 and will need to be reimplemented
- WebSocket handling code may need adjustments even though the API is similar

For more detailed information on specific topics, consult the full HTTP.jl v2.0.0 documentation.
For more detailed information on specific topics, consult the full HTTP.jl v2.0.0 documentation.
47 changes: 47 additions & 0 deletions docs/src/manual/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,53 @@ server = HTTP.serve!(handle_request, "127.0.0.1", 8080;
println("Server started on http://127.0.0.1:8080")
```

## Stream Handlers (Advanced)

If you need direct access to the request and response streams, pass `stream=true` to `serve!` (or `serve`) and use
an `HTTP.Stream` handler. Reads will automatically start when you read from the stream, and writes will automatically
start when you write.

```julia
using HTTP

server = HTTP.serve!("127.0.0.1", 8080; stream=true) do stream::HTTP.Stream
req = HTTP.startread(stream)
data = read(stream)

HTTP.setstatus(stream, 200)
HTTP.setheader(stream, "Content-Type" => "text/plain")
write(stream, "received $(length(data)) bytes")
HTTP.addtrailer(stream, "x-request-id" => HTTP.header(req, "x-request-id", "unknown"))
end
```

Trailing headers sent by the client are available after the request completes via `stream.request.trailers`.

## HTTP/2 Server Push (Advanced)

When handling an HTTP/2 request, you can send a push promise to the client and stream a pushed response.
The `HTTP.push_promise` function returns a new `HTTP.Stream` to write the pushed response.

```julia
using HTTP

HTTP.serve!("127.0.0.1", 8443; stream=true, ssl_cert="server.crt", ssl_key="server.key", ssl_alpn_list="h2") do stream
req = HTTP.startread(stream)
if stream.http2
push = HTTP.push_promise(stream, "GET", "/assets/app.js"; scheme="https", authority="127.0.0.1:8443")
HTTP.setstatus(push, 200)
HTTP.setheader(push, "Content-Type" => "application/javascript")
write(push, "console.log(\"pushed\")")
HTTP.closewrite(push)
end
HTTP.setstatus(stream, 200)
write(stream, "ok")
end
```

The push request must include `:scheme` and `:authority`. If these are not present on the original request,
pass them explicitly via the keyword arguments. Clients that do not accept server push will reject the promise.

## Handlers and Middleware

### Handler Functions
Expand Down
34 changes: 34 additions & 0 deletions docs/src/manual/websockets.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,40 @@ WebSockets.open("wss://echo.websocket.org") do ws
end
```

## Server-side WebSockets

For a dedicated WebSocket server, use `WebSockets.listen` or `WebSockets.listen!`:

```julia
using HTTP
using HTTP.WebSockets

server = WebSockets.listen!("127.0.0.1", 8080) do ws
for msg in ws
send(ws, msg)
end
end
```

To mix WebSockets and normal HTTP on the same port, use a stream handler with `HTTP.listen!` and
upgrade the connection when requested:

```julia
using HTTP
using HTTP.WebSockets

server = HTTP.listen!("127.0.0.1", 8080) do stream
if WebSockets.isupgrade(stream)
WebSockets.upgrade(stream) do ws
send(ws, "hello")
end
else
HTTP.setstatus(stream, 200)
write(stream, "ok")
end
end
```

## Connection Lifecycle and Error Handling

You can check whether a WebSocket is open using `WebSockets.isclosed(ws)` and close it with `close(ws)`. The API is designed to raise exceptions for connection issues or protocol errors, allowing you to handle errors using try‑catch blocks.
Expand Down
Loading
Loading