From d34407599961e1c775577e52f0ba9a3b1a352ea9 Mon Sep 17 00:00:00 2001 From: tan Date: Thu, 1 Oct 2026 10:59:44 +0530 Subject: [PATCH] docs(server): explain developing handlers with Revise via invokelatest middleware HTTP.jl 2.x no longer calls handlers through Base.invokelatest, so a running generated server keeps the method world it started in and does not see handler edits made with Revise. Document wrapping handlers with Base.invokelatest through register!'s middleware keyword, why it is not the default (it cannot be compiled with --trim), and add a test that a redefined handler is picked up without restarting the server. Closes #118 --- README.md | 3 +++ docs/src/servers.md | 26 ++++++++++++++++++++++++++ test/servergen.jl | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/README.md b/README.md index b5b1b92..4877e6e 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,9 @@ server = HTTP.serve!(router, "127.0.0.1", 8080) Requests are decoded and validated before handlers run; return values are validated and encoded from the documented responses. Generated stubs do not authenticate requests — apply a `middleware` for that. +To pick up handler edits made with Revise.jl in a running server, wrap the +handlers with `Base.invokelatest` through `middleware`; see +[Developing handlers with Revise](https://juliacomputing.github.io/OpenAPI.jl/stable/servers/#Developing-handlers-with-Revise). ## Create a document from Julia declarations diff --git a/docs/src/servers.md b/docs/src/servers.md index 197612d..d43ca75 100644 --- a/docs/src/servers.md +++ b/docs/src/servers.md @@ -112,6 +112,32 @@ handler contract — implementation module second, typed positional parameters, typed-value-or-`HTTP.Response` returns — matches the shape OpenAPI.jl 0.2.x users generated with `-g julia-server`. +## Developing handlers with Revise + +[Revise.jl](https://github.com/timholy/Revise.jl) updates handler methods, but +a running server keeps calling the definitions that existed when it started. +HTTP.jl 2.x does not call handlers through `Base.invokelatest`, so a server +task does not see methods defined after it began. Restart the server, or wrap +each handler with `Base.invokelatest` through `middleware` while developing: + +```julia +using Revise, HTTP +includet("Handlers.jl") # defines module Handlers +include("ExampleServer.jl") + +router = HTTP.Router() +ExampleServer.register!(router, Handlers; + middleware = handler -> (request -> Base.invokelatest(handler, request))) +server = HTTP.serve!(router, "127.0.0.1", 8080) +``` + +Once Revise applies an edit to `Handlers.jl` (before your next REPL command, +or when you call `Revise.revise()`), the next request uses the new definition +without a restart. +Generated servers do not do this by default because `Base.invokelatest` +cannot be compiled with Julia's `--trim` option, and generated servers stay +trim-compatible. Leave this middleware out of trimmed builds. + ## Choosing a response status A plain return value uses the first documented success response. When an diff --git a/test/servergen.jl b/test/servergen.jl index e21bb14..6bcd030 100644 --- a/test/servergen.jl +++ b/test/servergen.jl @@ -1318,3 +1318,38 @@ end close(server) end end + +@testset "invokelatest middleware picks up redefined handlers" begin + document = """ + openapi: 3.1.0 + info: {title: Revisable, version: 1.0.0} + paths: + /greet: + get: + operationId: greet + responses: + "200": + description: greeting + content: + text/plain: + schema: {type: string} + """ + host = Module(:RevisableServerHost) + Base.include_string(host, OpenAPI.server(document; name = "RevisableServer"), "RevisableServer.jl") + S = Base.invokelatest(getfield, host, :RevisableServer) + impl = Module(:RevisableImpl) + Core.eval(impl, :(greet(request) = "v1")) + router = HTTP.Router() + Base.invokelatest(Base.invokelatest(getfield, S, :register!), router, impl; + middleware = handler -> (request -> Base.invokelatest(handler, request))) + server = HTTP.serve!(router, "127.0.0.1", 0; verbose = false) + try + url = "http://127.0.0.1:$(HTTP.port(server))/greet" + @test String(HTTP.get(url).body) == "v1" + # Revise redefines methods in place; the running server must use the new one. + Core.eval(impl, :(greet(request) = "v2")) + @test String(HTTP.get(url).body) == "v2" + finally + close(server) + end +end