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