Migration From HTTP.jl 1.x
HTTP.jl 2.0 is a breaking release. Code that stayed on the common HTTP.get, HTTP.post, HTTP.request, and basic HTTP.serve! workflows should usually migrate with small edits. Code that reached into parser, connection-pool, layer-stack, HPACK, or HTTP/2 internals should move to the documented 2.0 API instead of chasing renamed internals.
The Before/After snippets below are intentionally minimal — they show the API shape change, not full runnable programs. Each runnable snippet assumes the caller has already issued using HTTP, plus any extra using statements shown in-line (for example using JSON for JSON examples or using Downloads for the HTTP.download discussion).
The most important 2.0 changes are:
- Julia 1.10 is the minimum supported Julia version.
- HTTP.jl now delegates transport, resolver, and TLS substrate work to Reseau.
HTTP.Headersis now a standalone mutable header struct rather than an alias for a vector of substring pairs.Request,Response,Headers,RequestContext, bodies,Client,Transport,Server, andStreamare the core public building blocks.- Top-level request helpers buffer
Response.body::Vector{UInt8}by default. RequestContextis typed request state, not a plainDict.- Client pooling, retries, TLS, proxying, and timeouts use more explicit
Client/Transport/ keyword configuration. - WebSocket entrypoints live under
HTTP.WebSockets.
Recommended Upgrade Order
- Upgrade Julia and dependency compat so HTTP.jl 2.0 can be resolved.
- Update high-level client calls and response field access.
- Update explicit
Request/Responseconstructors. - Replace direct connection-pool, layer, parser, HPACK, or HTTP/2 internals with documented
Client,Transport,Stream, server, or WebSocket APIs. - Re-test timeout, retry, proxy, cookie, streaming, WebSocket, SSE, and HTTP/2 paths explicitly.
High-Level Requests
Most simple request calls still look the same.
Before:
resp = HTTP.get(url)
text = String(resp.body)After:
resp = HTTP.get(url)
text = String(resp.body)By default, HTTP.request and verb helpers return a fully materialized Vector{UInt8} in resp.body. For streaming, use response_stream or HTTP.open.
Before:
open("payload.bin", "w") do io
HTTP.get(url; response_stream = io)
endAfter:
open("payload.bin", "w") do io
resp = HTTP.get(url; response_stream = io)
@assert resp.body === nothing
endIn 1.x, the supplied response_stream object could also appear as resp.body. In 2.0, the sink remains owned by the caller; read the data back from the original IO or byte buffer you passed as response_stream.
HTTP.download
The dedicated 1.x HTTP.download helper has been removed in HTTP.jl 2.0.
Because HTTP re-exports Base.download, calls like HTTP.download(url, path) continue to work. Those calls now go through Base.download (which is itself backed by the Downloads standard library), not through HTTP.jl's request stack. The 1.x keyword arguments (proxy, retry, headers, client, transport, TLS configuration, etc.) are not recognized by Base.download — passing them raises MethodError rather than silently being applied. Update such call sites to use one of the patterns below.
For the closest direct replacement, use the Downloads.download function from Julia's standard library:
using Downloads
Downloads.download(url, "payload.bin")If you want to keep all request handling inside HTTP.jl, stream the response body into an IOStream that you own:
using HTTP
open("payload.bin", "w") do io
resp = HTTP.request("GET", url; response_stream = io)
@assert 200 <= resp.status < 300
@assert resp.body === nothing
endUse this pattern when you need HTTP.jl client configuration such as custom headers, retries, proxy settings, TLS configuration, or a reusable HTTP.Client. Use Downloads.download when you just need a URL copied to a file.
For pull-based streaming:
using HTTP
HTTP.open(:GET, url) do stream
response = HTTP.startread(stream)
@info "status" response.status
output = IOBuffer()
buf = Vector{UInt8}(undef, 8192)
while true
n = readbytes!(stream, buf)
n == 0 && break
write(output, @view buf[1:n])
end
@info "captured bytes" length(take!(output))
endLike all HTTP.open do-block calls, the form above returns the final HTTP.Response — capture data you want to keep in an outer variable rather than as the value of the do block.
Request and Response Constructors
Common 1.x positional constructors remain accepted as migration shims, including string and byte-vector request or response bodies:
req = HTTP.Request("POST", "/widgets", ["Content-Type" => "text/plain"], "hello")
resp = HTTP.Response(201, ["Location" => "/widgets/1"], "created")New code may use the keyword forms when it needs explicit headers, trailers, protocol metadata, or context ownership:
req = HTTP.Request(
"POST",
"/widgets";
headers = ["Content-Type" => "text/plain"],
body = "hello",
)Headers
HTTP.Headers is the canonical mutable header container. It preserves pair order and canonicalizes header keys on insertion.
Before:
headers = ["content-type" => "application/json"]After:
headers = HTTP.Headers(["content-type" => "application/json"])
HTTP.setheader(headers, "x-request-id", request_id)Useful helpers include HTTP.header, HTTP.headers, HTTP.hasheader, HTTP.headercontains, HTTP.setheader, HTTP.appendheader, and HTTP.removeheader.
Request Context
In 1.x, middleware often treated request context as a plain dictionary. In 2.0, RequestContext is typed request state with deadline, cancellation, metadata, and timeout fields. Dict-like symbol-key metadata access still works for migration.
Before:
req.context[:request_id] = request_idAfter:
ctx = HTTP.get_request_context(req)
ctx[:request_id] = request_idReading application metadata remains familiar:
request_id = get(HTTP.get_request_context(req), :request_id, nothing)Use the typed helpers for control flow:
ctx = HTTP.get_request_context(req)
HTTP.set_deadline!(ctx, time_ns() + 5_000_000_000)
HTTP.cancel!(ctx; message = "caller disconnected")
HTTP.canceled(ctx) && throw(HTTP.CanceledError("request canceled"))For compatibility, req.context returns the metadata view. Use HTTP.get_request_context(req) whenever you need cancellation, deadlines, or timeout state.
Reusable Clients and Pooling
The 1.x pool keyword and old connection-pool internals are replaced by Client and Transport.
Before:
resp = HTTP.get(url; pool = pool)After:
transport = HTTP.Transport(max_idle_per_host = 4, max_idle_total = 32)
client = HTTP.Client(transport = transport, cookiejar = HTTP.CookieJar())
try
resp = HTTP.get(url; client = client)
finally
close(client)
endUse a long-lived Client when you want connection reuse, shared cookies, proxy configuration, retry buckets, and HTTP/2 preference to be consistent across many requests.
HTTP.@client
In 1.x, HTTP.@client composed one request middleware chain plus, optionally, a stream middleware chain. In 2.0, each position may be a single middleware or a tuple of middlewares:
HTTP.@client request_middleware
HTTP.@client request_middleware stream_middleware
HTTP.@client (outer_request, inner_request) (outer_stream, inner_stream)Request middlewares wrap high-level request(...) calls. Stream middlewares wrap HTTP.open(...) calls. Tuple entries are applied in order, so the first middleware listed is the outermost wrapper.
Retries
HTTP.jl 2.0 retries are explicit and conservative. The old retry_delays and retry_check keywords are accepted as compatibility shims, but new code should use retry, retries, retry_if, respect_retry_after, and HTTP.RetryBucket.
Before:
resp = HTTP.get(url; retry = true, retry_delays = [0.1, 0.5, 1.0])After:
bucket = HTTP.RetryBucket()
resp = HTTP.get(
url;
retry = true,
retries = 3,
retry_bucket = bucket,
respect_retry_after = true,
)Custom retry decisions move to retry_if.
function retry_if(attempt, err, req, resp)
if err !== nothing
return attempt <= 2
end
return resp !== nothing && resp.status == 503 && attempt <= 3
end
resp = HTTP.get(url; retry_if = retry_if)The full callback signature is:
retry_if(attempt::Integer, err, req::HTTP.Request, resp) -> Union{Bool,Nothing}attempt is the current one-based attempt number. err is a HTTP.RequestRetryError for request-path failures; inspect err.err for the underlying transport or protocol exception. Response-based decisions pass err = nothing and the response in resp. Returning true requests another attempt when the body can be replayed, false suppresses a retry, and nothing uses the built-in retry rules.
Timeouts
The 1.x readtimeout keyword is deprecated. It is still accepted, but now maps to read_idle_timeout.
Before:
resp = HTTP.get(url; connect_timeout = 5, readtimeout = 30)After:
resp = HTTP.get(
url;
connect_timeout = 5,
request_timeout = 60,
response_header_timeout = 10,
read_idle_timeout = 30,
)Use the timeout that matches your intent:
connect_timeoutbounds DNS, TCP connect, proxyCONNECT, TLS handshake, and HTTP/2 session setup.request_timeoutis the whole exchange deadline.response_header_timeoutbounds the wait for response headers.read_idle_timeoutbounds inactivity between inbound read progress events.write_idle_timeoutbounds inactivity between outbound write progress events.expect_continue_timeoutcontrols HTTP/1100-continueupload waits.
Timeout failures are reported as HTTP.HTTPTimeoutError, an alias for HTTP.TimeoutError.
TLS, Sockets, and Proxies
The old sslconfig and socket_type_tls extension points are retained for backwards compatibility only; they are no longer functional extension points for the 2.0 transport architecture. Configure TLS and socket behavior through the Reseau-backed Transport layer.
Before:
resp = HTTP.get(url; sslconfig = sslconfig)After:
transport = HTTP.Transport(tls_config = tls_config)
client = HTTP.Client(transport = transport)
resp = HTTP.get(url; client = client)Proxy configuration is more flexible and can be more explicit. As before, it can come from the environment, but callers can also pass a direct/no-proxy policy or a fixed proxy URL per request, client, or transport:
direct = HTTP.ProxyConfig()
from_env = HTTP.ProxyFromEnvironment()
fixed = HTTP.ProxyURL("http://proxy.internal:8080"; no_proxy = "localhost,127.0.0.1")
HTTP.get(url; proxy = from_env)
HTTP.get("http://127.0.0.1:8080"; proxy = direct)Servers
Request/response servers still use HTTP.serve!:
Before:
HTTP.serve!("127.0.0.1", 8080) do req
return HTTP.Response(200, "ok")
endAfter:
HTTP.serve!("127.0.0.1", 8080) do req
return HTTP.Response(200; body = "ok")
endUse HTTP.listen! for stream handlers:
server = HTTP.listen!("127.0.0.1", 8080) do stream
req = HTTP.startread(stream)
HTTP.setstatus(stream, 200)
HTTP.setheader(stream, "Content-Type", "text/plain")
write(stream, "streamed response for $(req.target)")
closewrite(stream)
HTTP.closeread(stream)
endEvery server timeout has a seconds-valued keyword and a nanosecond-valued _ns keyword. The old server readtimeout keyword is accepted as a seconds-valued migration alias for read_timeout.
server = HTTP.serve!(
handler,
"127.0.0.1",
8080;
read_timeout_ns = 30_000_000_000,
read_header_timeout_ns = 5_000_000_000,
write_timeout_ns = 30_000_000_000,
idle_timeout_ns = 120_000_000_000,
)
server = HTTP.serve!(
handler,
"127.0.0.1",
8080;
read_timeout = 30,
read_header_timeout = 5,
write_timeout = 30,
idle_timeout = 120,
)Useful server helpers in 2.0 include:
HTTP.fileserver(root)builds a ready-to-use static-file handler rooted atroot. It serves ordinary files, normalizes directory redirects, can serve a configured SPA fallback, and uses the same conditional/range-aware response helpers as the lower-level APIs.HTTP.servefile(request, path)serves one filesystem path for a request, includingIf-Modified-Since,If-None-Match,Range, content type,Last-Modified,ETag, andAccept-Rangeshandling.HTTP.servecontent(request, source)applies the same conditional and range response logic to bytes, strings, or seekableIOcontent you already own.HTTP.Routerfor route matching.HTTP.forceclose(server)for immediate shutdown.
Routing and Middleware
HTTP.Router and HTTP.register! are the top-level public router names; the underlying implementation lives in HTTP.Handlers and remains available as HTTP.Handlers.Router / HTTP.Handlers.register! if you prefer the explicit path.
router = HTTP.Router()
HTTP.register!(router, "GET", "/users/{id}") do req
id = HTTP.getparam(req, "id")
return HTTP.Response(200; body = id)
end
server = HTTP.serve!(router, "127.0.0.1", 8080)When a route matches, the route string and path parameters are stored in the request context. Retrieve them with HTTP.getroute, HTTP.getparams, or HTTP.getparam.
WebSockets
Use HTTP.WebSockets for WebSocket-specific client and server behavior. Top-level HTTP.open is for ordinary HTTP request/response streaming, not ws:// or wss:// URLs.
Before:
# Any code relying on top-level HTTP.open or internal upgrade helpers for
# WebSocket traffic should move to HTTP.WebSockets.After:
HTTP.WebSockets.open("ws://127.0.0.1:8080/socket") do ws
HTTP.WebSockets.send(ws, "ping")
msg = HTTP.WebSockets.receive(ws)
endServer side:
server = HTTP.WebSockets.listen!("127.0.0.1", 8080) do ws
for msg in ws
HTTP.WebSockets.send(ws, msg)
end
endThe WebSocket client accepts the handshake timeout controls connect_timeout, request_timeout, response_header_timeout, read_idle_timeout, and write_idle_timeout.
Server-Sent Events
Client-side SSE uses the sse_callback keyword on HTTP.request:
events = HTTP.SSEEvent[]
HTTP.request("GET", url; sse_callback = event -> push!(events, event))Server-side SSE uses HTTP.sse_stream:
HTTP.serve!("127.0.0.1", 8080) do req
return HTTP.sse_stream(200) do stream
write(stream, HTTP.SSEEvent("ready"; event = "status", id = "1"))
end
endInternal APIs
These 1.x internals are not migration targets for 2.0:
- layer-stack internals
- connection-pool internals
- parser internals
- undocumented socket/TLS extension points
Move those call sites to documented Client, Transport, Stream, server, router, WebSocket, or SSE APIs. If a 1.x internal use case cannot be expressed through the 2.0 public surface, open an issue with the use case rather than depending on the new internals.
Compatibility Keywords
HTTP.jl 2.0 accepts several old client keywords so existing code fails less abruptly:
readtimeout: maps toread_idle_timeoutpool: accepted, but useclient/transportretry_delaysandretry_check: accepted, but useretry_if,retries, andretry_bucketsslconfigandsocket_type_tls: accepted, but configure the transportcopyheaders,canonicalize_headers,detect_content_type,observelayers,logerrors, andlogtag: accepted for compatibility, but not the preferred 2.0 observation/configuration surface
Treat these as temporary migration aids. New code should use the documented 2.0 API names.
Final Checklist
- Prefer
resp.status;resp.status_coderemains available as a compatibility alias. - Use
HTTP.get_request_context(req)for cancellation/deadline state. - Prefer keyword constructors for
RequestandResponse. - Replace
poolusage with a long-livedHTTP.Client. - Replace
readtimeoutwith the precise timeout keyword you need. - Replace
HTTP.downloadwithDownloads.downloador an explicitHTTP.request(...; response_stream = io)file stream. - Move WebSocket code to
HTTP.WebSockets. - Replace internal parser/connection/HPACK/HTTP2 usage with documented APIs.
- Run integration tests for redirects, retries, proxy configuration, cookies, streaming, WebSockets, SSE, and HTTP/2 after upgrading.