From a6cf708727831d07653828fbe838a972720eabb4 Mon Sep 17 00:00:00 2001 From: krynju Date: Mon, 22 Jun 2026 12:08:56 +0200 Subject: [PATCH 1/9] Support HTTP.jl 2.0 alongside 1.x Add compatibility for HTTP.jl 2.0's reworked public API while keeping 1.x working. A load-time feature flag (_HTTP_V2) and small shims in the HTTP.jl client backend bridge the differences: - statustext: HTTP.Messages removed -> fall back to Response.reason - header lookup: HTTP.Header / HTTP.header(::Vector) removed -> plain case-insensitive lookup over the request headers - content_type returns a String on 2.0 (was a Pair on 1.x) - TimeoutError.readtimeout -> .timeout_ns; ConnectError.error -> .cause - timeout milliseconds forced to Integer so the reason string still matches the `\d+ milliseconds` regex in is_longpoll_timeout - read(io, n) removed -> readbytes!; drop the verbose kwarg unsupported by 2.0 HTTP.open; map readtimeout -> read_idle_timeout on 2.0 server.jl: HTTP.Forms.Multipart -> HTTP.Multipart (same type on 1.x). Tests/fixtures updated for both versions, and the timeout server fixture uses HTTP.listen! on 2.0 (serve!(...; stream=true) on 1.x). CI now tests both HTTP majors via an http matrix dimension; a step pins the HTTP compat per cell. Julia 1.6 + HTTP 2 is excluded (HTTP 2.0 requires Julia >= 1.10). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 11 ++- Project.toml | 2 +- src/client/httplibs/juliaweb_http.jl | 86 +++++++++++++++---- src/server.jl | 4 +- test/client/utilstests.jl | 12 ++- test/deep_object/deep_server.jl | 2 +- test/server/timeouttest/timeouttest_server.jl | 6 +- 7 files changed, 94 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17d7e3c..d1b4eb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: pull_request: jobs: test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + name: Julia ${{ matrix.version }} - HTTP ${{ matrix.http }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -15,10 +15,17 @@ jobs: - '1.6' - '1' # automatically expands to the latest stable 1.x release of Julia - 'nightly' + http: + - '1' + - '2' os: - ubuntu-latest arch: - x64 + exclude: + # HTTP.jl 2.0 requires Julia >= 1.10 + - version: '1.6' + http: '2' steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 @@ -35,6 +42,8 @@ jobs: ${{ runner.os }}-test-${{ env.cache-name }}- ${{ runner.os }}-test- ${{ runner.os }}- + - name: Constrain HTTP.jl to v${{ matrix.http }} + run: sed -i -E 's/^HTTP = .*/HTTP = "${{ matrix.http }}"/' Project.toml - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 diff --git a/Project.toml b/Project.toml index 2d7e47b..fad5b3e 100644 --- a/Project.toml +++ b/Project.toml @@ -21,7 +21,7 @@ p7zip_jll = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" [compat] Downloads = "1" -HTTP = "1" +HTTP = "1, 2" JSON = "0.20, 0.21, 1" LibCURL = "0.6, 1" MIMEs = "0.1, 1" diff --git a/src/client/httplibs/juliaweb_http.jl b/src/client/httplibs/juliaweb_http.jl index a856b75..688366f 100644 --- a/src/client/httplibs/juliaweb_http.jl +++ b/src/client/httplibs/juliaweb_http.jl @@ -22,9 +22,49 @@ # - HTTPRequestError <: AbstractHTTPLibError # ============================================================================= +# HTTP.jl 2.0 dropped the `HTTP.Messages` submodule (and several other 1.x +# symbols). We support both 1.x and 2.x by feature-detecting at load time. +const _HTTP_V2 = !isdefined(HTTP, :Messages) + +# Status reason text: `HTTP.Messages.statustext` on 1.x, `Response.reason` on 2.x. +function _http_statustext(raw::HTTP.Response) + if isdefined(HTTP, :Messages) + return HTTP.Messages.statustext(raw.status) + elseif hasproperty(raw, :reason) && !isempty(raw.reason) + return raw.reason + else + return string(raw.status) + end +end + +# Case-insensitive header lookup over the request header collection (a Dict). +# Avoids `HTTP.Header`/`HTTP.header(::Vector, ...)`, both removed in 2.0. +function _header_value_ci(headers, key::AbstractString) + lk = lowercase(key) + for (k, v) in headers + lowercase(String(k)) == lk && return String(v) + end + return nothing +end + +# Form content type: `HTTP.content_type` returns a `Pair` on 1.x, a `String` on 2.x. +_http_form_content_type(body) = (ct = HTTP.content_type(body); ct isa Pair ? ct[2] : ct) + +# Timeout budget in ms: `TimeoutError.readtimeout` (s) on 1.x, `.timeout_ns` on 2.x. +# Keep this an integer — the reason string is later matched against `\d+ milliseconds`. +_http_timeout_ms(e::HTTP.TimeoutError) = + hasproperty(e, :readtimeout) ? e.readtimeout * 1000 : e.timeout_ns ÷ 1_000_000 + +# Underlying cause of a connect error: `.error` on 1.x, `.cause` on 2.x. +_http_connect_cause(e::HTTP.ConnectError) = hasproperty(e, :cause) ? e.cause : e.error + +# Inactivity-timeout keyword: 1.x calls it `readtimeout`; 2.0 renamed it to +# `read_idle_timeout` (`readtimeout` still works but emits a deprecation warning). +_http_read_timeout_kw(timeout) = _HTTP_V2 ? (; read_idle_timeout=timeout) : (; readtimeout=timeout) + function get_response_property(raw::HTTP.Response, name::Symbol) if name === :message - return HTTP.Messages.statustext(raw.status) + return _http_statustext(raw) else return getproperty(raw, name) end @@ -40,20 +80,21 @@ struct HTTPRequestError <: AbstractHTTPLibError response::Union{Nothing,HTTP.Response} function HTTPRequestError(error::HTTP.TimeoutError, bytesread::Int, response::Union{Nothing,HTTP.Response}) - message = "Operation timed out after $(error.readtimeout*1000) milliseconds with $(bytesread) bytes received" + message = "Operation timed out after $(_http_timeout_ms(error)) milliseconds with $(bytesread) bytes received" new(message, error, response) end function HTTPRequestError(error::HTTP.TimeoutError, response::Union{Nothing,HTTP.Response}) - message = "Operation timed out after $(error.readtimeout*1000) milliseconds" + message = "Operation timed out after $(_http_timeout_ms(error)) milliseconds" new(message, error, response) end function HTTPRequestError(error::HTTP.ConnectError) - message = if isa(error.error, CapturedException) - string(error.error.ex) + cause = _http_connect_cause(error) + message = if isa(cause, CapturedException) + string(cause.ex) else - string(error.error) + string(cause) end new(message, error, nothing) end @@ -99,8 +140,7 @@ function prep_args(::Val{:http}, ctx::Ctx) headers = ctx.header body = nothing - header_pairs = [convert(HTTP.Header, p) for p in headers] - content_type_set = HTTP.header(header_pairs, "Content-Type", nothing) + content_type_set = _header_value_ci(headers, "Content-Type") if !isnothing(content_type_set) content_type_set = lowercase(content_type_set) end @@ -146,7 +186,7 @@ function prep_args(::Val{:http}, ctx::Ctx) body_dict[_k] = _v end body = HTTP.Form(body_dict) - headers["Content-Type"] = content_type_set = HTTP.content_type(body)[2] + headers["Content-Type"] = content_type_set = _http_form_content_type(body) end if ctx.body !== nothing @@ -213,7 +253,7 @@ end function _http_request(ctx, method, url, headers, body, timeout, bytesread, captured_response, output) captured_response[] = http_response = HTTP.request(method, url, headers, body; - readtimeout=timeout, + _http_read_timeout_kw(timeout)..., connect_timeout=timeout ÷ 2, retry=false, redirect=true, @@ -229,22 +269,30 @@ end function _http_streaming_request(ctx, method, url, headers, body, timeout, bytesread, captured_response, output, stream_to) http_response = nothing + # HTTP.jl 2.0's `HTTP.open` does not accept a `verbose` keyword; only pass it on 1.x. + open_kwargs = merge(_http_read_timeout_kw(timeout), + (; connect_timeout=timeout ÷ 2, + retry=false, + redirect=true, + status_exception=false)) + if !_HTTP_V2 + open_kwargs = merge(open_kwargs, (; verbose=get(ctx.client.clntoptions, :verbose, false))) + end + @sync begin @async begin try - HTTP.open(method, url, headers; - readtimeout=timeout, - connect_timeout=timeout ÷ 2, - retry=false, - redirect=true, - status_exception=false, - verbose=get(ctx.client.clntoptions, :verbose, false)) do io + HTTP.open(method, url, headers; open_kwargs...) do io write(io, body) captured_response[] = http_response = startread(io) try + # `read(io, n)` is unavailable on 2.0 streams; read into a reusable + # buffer with `readbytes!`, which works on both 1.x and 2.x. + buf = Vector{UInt8}(undef, 8192) # 8KB chunks while !eof(io) - data = read(io, 8192) # Read 8KB chunks - bytesread[] += write(output, data) + n = readbytes!(io, buf) + n == 0 && break + bytesread[] += write(output, view(buf, 1:n)) end finally close(output) diff --git a/src/server.jl b/src/server.jl index d7d1a74..22e2e31 100644 --- a/src/server.jl +++ b/src/server.jl @@ -67,7 +67,7 @@ function get_param(source::Dict, name::String, required::Bool) return val end -function get_param(source::Vector{HTTP.Forms.Multipart}, name::String, required::Bool) +function get_param(source::Vector{HTTP.Multipart}, name::String, required::Bool) ind = findfirst(x -> x.name == name, source) if required && isnothing(ind) throw(ValidationException("required parameter \"$name\" missing")) @@ -140,7 +140,7 @@ function to_param(T, source::Dict, name::String; required::Bool=false, collectio end end -function to_param(T, source::Vector{HTTP.Forms.Multipart}, name::String; required::Bool=false, collection_format::Union{String,Nothing}=",", multipart::Bool=false, isfile::Bool=false) +function to_param(T, source::Vector{HTTP.Multipart}, name::String; required::Bool=false, collection_format::Union{String,Nothing}=",", multipart::Bool=false, isfile::Bool=false) param = get_param(source, name, required) if param === nothing return nothing diff --git a/test/client/utilstests.jl b/test/client/utilstests.jl index af8f108..3c065ae 100644 --- a/test/client/utilstests.jl +++ b/test/client/utilstests.jl @@ -68,16 +68,20 @@ end function test_longpoll_exception_check() resp = OpenAPI.Clients.Downloads.Response("http", "http://localhost", 200, "no error", []) + # HTTP.jl 1.x and 2.0 have different `TimeoutError`/`ConnectError` constructors. + mk_timeout_err() = OpenAPI.Clients._HTTP_V2 ? HTTP.TimeoutError("read_idle", 20_000_000) : HTTP.TimeoutError(20) + mk_connect_err() = HTTP.ConnectError("http://localhost", ErrorException("dns error")) + not_longpoll_timeouts = [ OpenAPI.Clients.Downloads.RequestError("http://localhost", 500, "not timeout error", resp), - OpenAPI.Clients.HTTPRequestError(HTTP.TimeoutError(20), 20, nothing), - OpenAPI.Clients.HTTPRequestError(HTTP.TimeoutError(20), nothing), - OpenAPI.Clients.HTTPRequestError(HTTP.ConnectError("http://localhost", "dns error")), + OpenAPI.Clients.HTTPRequestError(mk_timeout_err(), 20, nothing), + OpenAPI.Clients.HTTPRequestError(mk_timeout_err(), nothing), + OpenAPI.Clients.HTTPRequestError(mk_connect_err()), ] longpoll_timeouts = [ OpenAPI.Clients.Downloads.RequestError("http://localhost", 200, "Operation timed out after 300 milliseconds with 0 bytes received", resp), # timeout error - OpenAPI.Clients.HTTPRequestError(HTTP.TimeoutError(20), 20, HTTP.Response(200, "hello")), + OpenAPI.Clients.HTTPRequestError(mk_timeout_err(), 20, HTTP.Response(200, "hello")), ] @test OpenAPI.Clients.is_longpoll_timeout("not an exception") == false diff --git a/test/deep_object/deep_server.jl b/test/deep_object/deep_server.jl index 8d56254..2c02e8d 100644 --- a/test/deep_object/deep_server.jl +++ b/test/deep_object/deep_server.jl @@ -6,7 +6,7 @@ using .DeepServer: register, FindPetsByStatus200Response const server = Ref{Any}(nothing) -function find_pets_by_status(::HTTP.Messages.Request, param::DeepServer.FindPetsByStatusStatusParameter) +function find_pets_by_status(::HTTP.Request, param::DeepServer.FindPetsByStatusStatusParameter) return FindPetsByStatus200Response(param) end diff --git a/test/server/timeouttest/timeouttest_server.jl b/test/server/timeouttest/timeouttest_server.jl index df9702b..02bf18c 100644 --- a/test/server/timeouttest/timeouttest_server.jl +++ b/test/server/timeouttest/timeouttest_server.jl @@ -51,7 +51,11 @@ function run_server(port=8081) HTTP.register!(router, "/longpollstream", longpollstream) HTTP.register!(router, "/stop", HTTP.streamhandler(stop)) HTTP.register!(router, "/ping", HTTP.streamhandler(ping)) - server[] = HTTP.serve!(router, port; stream=true) + # HTTP.jl 1.x serves stream handlers via `serve!(...; stream=true)`; + # 2.0 uses `listen!`, which always runs the handler in streaming mode. + server[] = isdefined(HTTP, :Messages) ? + HTTP.serve!(router, port; stream=true) : + HTTP.listen!(router, "127.0.0.1", port) wait(server[]) catch ex @error("Server error", exception=(ex, catch_backtrace())) From 00a254161f43d00d6fc8f13b09bca9d5ea24a165 Mon Sep 17 00:00:00 2001 From: krynju Date: Mon, 22 Jun 2026 12:20:40 +0200 Subject: [PATCH 2/9] Fix CI HTTP pin: scope sed to [compat] section The unscoped sed matched the HTTP UUID line in [deps] too, rewriting it to a bare version and breaking resolution (Malformed UUID string). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1b4eb7..09f331b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: ${{ runner.os }}-test- ${{ runner.os }}- - name: Constrain HTTP.jl to v${{ matrix.http }} - run: sed -i -E 's/^HTTP = .*/HTTP = "${{ matrix.http }}"/' Project.toml + run: sed -i -E '/^\[compat\]/,/^\[/{s/^HTTP = .*/HTTP = "${{ matrix.http }}"/}' Project.toml - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 From 23808282234b6f71803c81616589d3f7fcf15bbd Mon Sep 17 00:00:00 2001 From: krynju Date: Mon, 22 Jun 2026 15:13:23 +0200 Subject: [PATCH 3/9] Detect HTTP major via pkgversion instead of submodule sniffing Use pkgversion(HTTP) >= v"2" (guarded for Julia < 1.9, where only HTTP 1.x can be installed) rather than checking for the internal HTTP.Messages submodule. Explicit about intent and not coupled to an internal name. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/client/httplibs/juliaweb_http.jl | 7 ++++--- test/server/timeouttest/timeouttest_server.jl | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/client/httplibs/juliaweb_http.jl b/src/client/httplibs/juliaweb_http.jl index 688366f..9a50d33 100644 --- a/src/client/httplibs/juliaweb_http.jl +++ b/src/client/httplibs/juliaweb_http.jl @@ -22,9 +22,10 @@ # - HTTPRequestError <: AbstractHTTPLibError # ============================================================================= -# HTTP.jl 2.0 dropped the `HTTP.Messages` submodule (and several other 1.x -# symbols). We support both 1.x and 2.x by feature-detecting at load time. -const _HTTP_V2 = !isdefined(HTTP, :Messages) +# HTTP.jl 2.0 reworked much of the public API; we support both 1.x and 2.x and +# branch on the major version at load time. `pkgversion` exists from Julia 1.9; +# on older Julia only HTTP 1.x can be installed (HTTP 2.0 needs Julia >= 1.10). +const _HTTP_V2 = isdefined(Base, :pkgversion) && something(pkgversion(HTTP), v"1") >= v"2" # Status reason text: `HTTP.Messages.statustext` on 1.x, `Response.reason` on 2.x. function _http_statustext(raw::HTTP.Response) diff --git a/test/server/timeouttest/timeouttest_server.jl b/test/server/timeouttest/timeouttest_server.jl index 02bf18c..2ac8dd9 100644 --- a/test/server/timeouttest/timeouttest_server.jl +++ b/test/server/timeouttest/timeouttest_server.jl @@ -53,9 +53,10 @@ function run_server(port=8081) HTTP.register!(router, "/ping", HTTP.streamhandler(ping)) # HTTP.jl 1.x serves stream handlers via `serve!(...; stream=true)`; # 2.0 uses `listen!`, which always runs the handler in streaming mode. - server[] = isdefined(HTTP, :Messages) ? - HTTP.serve!(router, port; stream=true) : - HTTP.listen!(router, "127.0.0.1", port) + http_v2 = isdefined(Base, :pkgversion) && something(pkgversion(HTTP), v"1") >= v"2" + server[] = http_v2 ? + HTTP.listen!(router, "127.0.0.1", port) : + HTTP.serve!(router, port; stream=true) wait(server[]) catch ex @error("Server error", exception=(ex, catch_backtrace())) From 9cd68f108aa4682e0445ca74974a520de0b91ffa Mon Sep 17 00:00:00 2001 From: tan Date: Tue, 23 Jun 2026 10:18:31 +0530 Subject: [PATCH 4/9] Normalize HTTP 2.x DNSError reason to match 1.x HTTP.jl 2.x introduced a dedicated HTTP.DNSError type for name-resolution failures. It stringifies as "HTTP.DNSError(...)", whereas 1.x wrapped a Sockets.DNSError in a ConnectError that stringified as "DNSError: ...". This broke the client's reason-string contract (tests assert the reason starts with "DNSError"). Add a version-guarded _http_error_message specialization that normalizes the 2.x DNSError into "DNSError: could not resolve host """. Elided under 1.x via @static if _HTTP_V2, so 1.x behavior is unchanged. --- src/client/httplibs/juliaweb_http.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/client/httplibs/juliaweb_http.jl b/src/client/httplibs/juliaweb_http.jl index 9a50d33..57a5759 100644 --- a/src/client/httplibs/juliaweb_http.jl +++ b/src/client/httplibs/juliaweb_http.jl @@ -59,6 +59,16 @@ _http_timeout_ms(e::HTTP.TimeoutError) = # Underlying cause of a connect error: `.error` on 1.x, `.cause` on 2.x. _http_connect_cause(e::HTTP.ConnectError) = hasproperty(e, :cause) ? e.cause : e.error +# Message for a generic HTTP error. HTTP 2.x introduces a dedicated `HTTP.DNSError` +# (a subtype of HTTP.HTTPError) for name-resolution failures, where 1.x instead wraps a +# `Sockets.DNSError` inside a ConnectError. The two stringify differently +# (`"HTTP.DNSError(...)"` vs `"DNSError: ..."`); normalize the 2.x form so the surfaced +# reason starts with "DNSError" on both, keeping the message stable across versions. +_http_error_message(error::HTTP.HTTPError) = string(error) +@static if _HTTP_V2 + _http_error_message(error::HTTP.DNSError) = "DNSError: could not resolve host \"$(error.hostname)\"" +end + # Inactivity-timeout keyword: 1.x calls it `readtimeout`; 2.0 renamed it to # `read_idle_timeout` (`readtimeout` still works but emits a deprecation warning). _http_read_timeout_kw(timeout) = _HTTP_V2 ? (; read_idle_timeout=timeout) : (; readtimeout=timeout) @@ -101,7 +111,7 @@ struct HTTPRequestError <: AbstractHTTPLibError end function HTTPRequestError(error::HTTP.HTTPError) - message = string(error) + message = _http_error_message(error) new(message, error, nothing) end end From b83e09a7301fdc67b7f5c9be4bc1c220a5bac400 Mon Sep 17 00:00:00 2001 From: tan Date: Tue, 23 Jun 2026 11:23:01 +0530 Subject: [PATCH 5/9] Fix generated server code for HTTP 2: HTTP.headers(req) removed HTTP.jl 2.x removed the 1-arg HTTP.headers(req) form that returns all headers; only the 2-arg lookup headers(req, key) remains. Generated server read-handlers used Dict{String,String}(HTTP.headers(req)) to collect header params, so any endpoint with a header param (e.g. delete_pet's api_key) threw a MethodError -> HTTP 500 under HTTP 2. Use req.headers, which works on both 1.x and 2.x. NOTE: this is generated code. The matching change belongs upstream in the openapi-generator Julia server template; these checked-in fixtures are hand-patched to match until that lands. --- test/opa/OPAServer/src/apis/api_DataAPIApi.jl | 205 ++++++++++++ .../src/apis/api_JobRunnerApi.jl | 298 ++++++++++++++++++ .../petstore/src/apis/api_PetApi.jl | 2 +- .../petstore/src/apis/api_PetApi.jl | 2 +- .../petstore/src/apis/api_PetApi.jl | 2 +- 5 files changed, 506 insertions(+), 3 deletions(-) create mode 100644 test/opa/OPAServer/src/apis/api_DataAPIApi.jl create mode 100644 test/server/juliahub/specs/juliahubrunner/src/apis/api_JobRunnerApi.jl diff --git a/test/opa/OPAServer/src/apis/api_DataAPIApi.jl b/test/opa/OPAServer/src/apis/api_DataAPIApi.jl new file mode 100644 index 0000000..0eb8aa3 --- /dev/null +++ b/test/opa/OPAServer/src/apis/api_DataAPIApi.jl @@ -0,0 +1,205 @@ +# This file was generated by the Julia OpenAPI Code Generator +# Do not modify this file directly. Modify the OpenAPI specification instead. + + +function delete_document_read(handler) + function delete_document_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function delete_document_validate(handler) + function delete_document_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + return handler(req) + end +end + +function delete_document_invoke(impl; post_invoke=nothing) + function delete_document_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.delete_document(req::HTTP.Request, openapi_params["path"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function get_document_read(handler) + function get_document_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) + query_params = HTTP.queryparams(URIs.URI(req.target)) + openapi_params["input"] = OpenAPI.Servers.to_param(Dict{String, Any}, query_params, "input", ) + openapi_params["pretty"] = OpenAPI.Servers.to_param(Bool, query_params, "pretty", ) + openapi_params["provenance"] = OpenAPI.Servers.to_param(Bool, query_params, "provenance", ) + openapi_params["explain"] = OpenAPI.Servers.to_param(String, query_params, "explain", ) + openapi_params["metrics"] = OpenAPI.Servers.to_param(Bool, query_params, "metrics", ) + openapi_params["instrument"] = OpenAPI.Servers.to_param(Bool, query_params, "instrument", ) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function get_document_validate(handler) + function get_document_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + return handler(req) + end +end + +function get_document_invoke(impl; post_invoke=nothing) + function get_document_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.get_document(req::HTTP.Request, openapi_params["path"]; input=get(openapi_params, "input", nothing), pretty=get(openapi_params, "pretty", nothing), provenance=get(openapi_params, "provenance", nothing), explain=get(openapi_params, "explain", nothing), metrics=get(openapi_params, "metrics", nothing), instrument=get(openapi_params, "instrument", nothing),) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function get_document_with_path_read(handler) + function get_document_with_path_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) + query_params = HTTP.queryparams(URIs.URI(req.target)) + openapi_params["pretty"] = OpenAPI.Servers.to_param(Bool, query_params, "pretty", ) + openapi_params["provenance"] = OpenAPI.Servers.to_param(Bool, query_params, "provenance", ) + openapi_params["explain"] = OpenAPI.Servers.to_param(String, query_params, "explain", ) + openapi_params["metrics"] = OpenAPI.Servers.to_param(Bool, query_params, "metrics", ) + openapi_params["instrument"] = OpenAPI.Servers.to_param(Bool, query_params, "instrument", ) + openapi_params["request_body"] = OpenAPI.Servers.to_param_type(Dict{String, Any}, String(req.body)) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function get_document_with_path_validate(handler) + function get_document_with_path_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + return handler(req) + end +end + +function get_document_with_path_invoke(impl; post_invoke=nothing) + function get_document_with_path_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.get_document_with_path(req::HTTP.Request, openapi_params["path"], openapi_params["request_body"]; pretty=get(openapi_params, "pretty", nothing), provenance=get(openapi_params, "provenance", nothing), explain=get(openapi_params, "explain", nothing), metrics=get(openapi_params, "metrics", nothing), instrument=get(openapi_params, "instrument", nothing),) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function get_document_with_web_hook_read(handler) + function get_document_with_web_hook_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) + query_params = HTTP.queryparams(URIs.URI(req.target)) + openapi_params["pretty"] = OpenAPI.Servers.to_param(Bool, query_params, "pretty", ) + openapi_params["request_body"] = OpenAPI.Servers.to_param_type(Dict{String, Any}, String(req.body)) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function get_document_with_web_hook_validate(handler) + function get_document_with_web_hook_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + return handler(req) + end +end + +function get_document_with_web_hook_invoke(impl; post_invoke=nothing) + function get_document_with_web_hook_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.get_document_with_web_hook(req::HTTP.Request, openapi_params["path"], openapi_params["request_body"]; pretty=get(openapi_params, "pretty", nothing),) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function patch_document_read(handler) + function patch_document_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) + openapi_params["PatchesSchemaInner"] = OpenAPI.Servers.to_param_type(Vector{PatchesSchemaInner}, String(req.body)) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function patch_document_validate(handler) + function patch_document_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + OpenAPI.validate_param("PatchesSchemaInner", "patch_document", :minItems, openapi_params["PatchesSchemaInner"], 1) + + return handler(req) + end +end + +function patch_document_invoke(impl; post_invoke=nothing) + function patch_document_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.patch_document(req::HTTP.Request, openapi_params["path"], openapi_params["PatchesSchemaInner"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function put_document_read(handler) + function put_document_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["path"] = OpenAPI.Servers.to_param(String, path_params, "path", required=true, ) + headers = Dict{String,String}(req.headers) + openapi_params["If-None-Match"] = OpenAPI.Servers.to_param(String, headers, "If-None-Match", ) + openapi_params["body"] = OpenAPI.Servers.to_param_type(Any, String(req.body)) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function put_document_validate(handler) + function put_document_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + return handler(req) + end +end + +function put_document_invoke(impl; post_invoke=nothing) + function put_document_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.put_document(req::HTTP.Request, openapi_params["path"], openapi_params["body"]; if_none_match=get(openapi_params, "If-None-Match", nothing),) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + + +function registerDataAPIApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) + HTTP.register!(router, "DELETE", path_prefix * "/v1/data/{path}", OpenAPI.Servers.middleware(impl, delete_document_read, delete_document_validate, delete_document_invoke; optional_middlewares...)) + HTTP.register!(router, "GET", path_prefix * "/v1/data/{path}", OpenAPI.Servers.middleware(impl, get_document_read, get_document_validate, get_document_invoke; optional_middlewares...)) + HTTP.register!(router, "POST", path_prefix * "/v1/data/{path}", OpenAPI.Servers.middleware(impl, get_document_with_path_read, get_document_with_path_validate, get_document_with_path_invoke; optional_middlewares...)) + HTTP.register!(router, "POST", path_prefix * "/v0/data/{path}", OpenAPI.Servers.middleware(impl, get_document_with_web_hook_read, get_document_with_web_hook_validate, get_document_with_web_hook_invoke; optional_middlewares...)) + HTTP.register!(router, "PATCH", path_prefix * "/v1/data/{path}", OpenAPI.Servers.middleware(impl, patch_document_read, patch_document_validate, patch_document_invoke; optional_middlewares...)) + HTTP.register!(router, "PUT", path_prefix * "/v1/data/{path}", OpenAPI.Servers.middleware(impl, put_document_read, put_document_validate, put_document_invoke; optional_middlewares...)) + return router +end diff --git a/test/server/juliahub/specs/juliahubrunner/src/apis/api_JobRunnerApi.jl b/test/server/juliahub/specs/juliahubrunner/src/apis/api_JobRunnerApi.jl new file mode 100644 index 0000000..b28304d --- /dev/null +++ b/test/server/juliahub/specs/juliahubrunner/src/apis/api_JobRunnerApi.jl @@ -0,0 +1,298 @@ +# This file was generated by the Julia OpenAPI Code Generator +# Do not modify this file directly. Modify the OpenAPI specification instead. + + +function file_upload_finalize_read(handler) + function file_upload_finalize_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) + openapi_params["UploadFileDetails"] = OpenAPI.Servers.to_param_type(UploadFileDetails, String(req.body)) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function file_upload_finalize_validate(handler) + function file_upload_finalize_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + OpenAPI.validate_param("job_id", "file_upload_finalize", :maxLength, openapi_params["job_id"], 13) + OpenAPI.validate_param("job_id", "file_upload_finalize", :minLength, openapi_params["job_id"], 10) + + return handler(req) + end +end + +function file_upload_finalize_invoke(impl; post_invoke=nothing) + function file_upload_finalize_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.file_upload_finalize(req::HTTP.Request, openapi_params["job_id"], openapi_params["UploadFileDetails"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function file_upload_init_read(handler) + function file_upload_init_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) + openapi_params["UploadFileDetails"] = OpenAPI.Servers.to_param_type(UploadFileDetails, String(req.body)) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function file_upload_init_validate(handler) + function file_upload_init_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + OpenAPI.validate_param("job_id", "file_upload_init", :maxLength, openapi_params["job_id"], 13) + OpenAPI.validate_param("job_id", "file_upload_init", :minLength, openapi_params["job_id"], 10) + + return handler(req) + end +end + +function file_upload_init_invoke(impl; post_invoke=nothing) + function file_upload_init_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.file_upload_init(req::HTTP.Request, openapi_params["job_id"], openapi_params["UploadFileDetails"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function get_clusterinfo_read(handler) + function get_clusterinfo_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function get_clusterinfo_validate(handler) + function get_clusterinfo_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + OpenAPI.validate_param("job_id", "get_clusterinfo", :maxLength, openapi_params["job_id"], 13) + OpenAPI.validate_param("job_id", "get_clusterinfo", :minLength, openapi_params["job_id"], 10) + + return handler(req) + end +end + +function get_clusterinfo_invoke(impl; post_invoke=nothing) + function get_clusterinfo_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.get_clusterinfo(req::HTTP.Request, openapi_params["job_id"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function get_dataset_credentials_read(handler) + function get_dataset_credentials_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["dataset_id"] = OpenAPI.Servers.to_param(String, path_params, "dataset_id", required=true, ) + headers = Dict{String,String}(req.headers) + openapi_params["X-JuliaHub-JobId"] = OpenAPI.Servers.to_param(String, headers, "X-JuliaHub-JobId", required=true, ) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function get_dataset_credentials_validate(handler) + function get_dataset_credentials_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + OpenAPI.validate_param("dataset_id", "get_dataset_credentials", :maxLength, openapi_params["dataset_id"], 36) + OpenAPI.validate_param("dataset_id", "get_dataset_credentials", :minLength, openapi_params["dataset_id"], 36) + + OpenAPI.validate_param("X-JuliaHub-JobId", "get_dataset_credentials", :maxLength, openapi_params["X-JuliaHub-JobId"], 13) + OpenAPI.validate_param("X-JuliaHub-JobId", "get_dataset_credentials", :minLength, openapi_params["X-JuliaHub-JobId"], 10) + + return handler(req) + end +end + +function get_dataset_credentials_invoke(impl; post_invoke=nothing) + function get_dataset_credentials_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.get_dataset_credentials(req::HTTP.Request, openapi_params["dataset_id"], openapi_params["X-JuliaHub-JobId"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function get_datasets_read(handler) + function get_datasets_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function get_datasets_validate(handler) + function get_datasets_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + return handler(req) + end +end + +function get_datasets_invoke(impl; post_invoke=nothing) + function get_datasets_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.get_datasets(req::HTTP.Request;) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function get_job_input_read(handler) + function get_job_input_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function get_job_input_validate(handler) + function get_job_input_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + OpenAPI.validate_param("job_id", "get_job_input", :maxLength, openapi_params["job_id"], 13) + OpenAPI.validate_param("job_id", "get_job_input", :minLength, openapi_params["job_id"], 10) + + return handler(req) + end +end + +function get_job_input_invoke(impl; post_invoke=nothing) + function get_job_input_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.get_job_input(req::HTTP.Request, openapi_params["job_id"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function get_user_datasets_read(handler) + function get_user_datasets_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + query_params = HTTP.queryparams(URIs.URI(req.target)) + openapi_params["name"] = OpenAPI.Servers.to_param(String, query_params, "name", style="form", is_explode=true) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function get_user_datasets_validate(handler) + function get_user_datasets_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + + return handler(req) + end +end + +function get_user_datasets_invoke(impl; post_invoke=nothing) + function get_user_datasets_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.get_user_datasets(req::HTTP.Request; name=get(openapi_params, "name", nothing),) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function update_job_distributed_status_read(handler) + function update_job_distributed_status_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) + openapi_params["JobDistributedStatus"] = OpenAPI.Servers.to_param_type(JobDistributedStatus, String(req.body)) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function update_job_distributed_status_validate(handler) + function update_job_distributed_status_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + OpenAPI.validate_param("job_id", "update_job_distributed_status", :maxLength, openapi_params["job_id"], 13) + OpenAPI.validate_param("job_id", "update_job_distributed_status", :minLength, openapi_params["job_id"], 10) + + return handler(req) + end +end + +function update_job_distributed_status_invoke(impl; post_invoke=nothing) + function update_job_distributed_status_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.update_job_distributed_status(req::HTTP.Request, openapi_params["job_id"], openapi_params["JobDistributedStatus"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +function update_job_status_read(handler) + function update_job_status_read_handler(req::HTTP.Request) + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["job_id"] = OpenAPI.Servers.to_param(String, path_params, "job_id", required=true, ) + openapi_params["JobStatus"] = OpenAPI.Servers.to_param_type(JobStatus, String(req.body)) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +function update_job_status_validate(handler) + function update_job_status_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + + OpenAPI.validate_param("job_id", "update_job_status", :maxLength, openapi_params["job_id"], 13) + OpenAPI.validate_param("job_id", "update_job_status", :minLength, openapi_params["job_id"], 10) + + return handler(req) + end +end + +function update_job_status_invoke(impl; post_invoke=nothing) + function update_job_status_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.update_job_status(req::HTTP.Request, openapi_params["job_id"], openapi_params["JobStatus"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + + +function registerJobRunnerApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) + HTTP.register!(router, "POST", path_prefix * "/jobs/{job_id}/file_uploads", OpenAPI.Servers.middleware(impl, file_upload_finalize_read, file_upload_finalize_validate, file_upload_finalize_invoke; optional_middlewares...)) + HTTP.register!(router, "PUT", path_prefix * "/jobs/{job_id}/file_uploads", OpenAPI.Servers.middleware(impl, file_upload_init_read, file_upload_init_validate, file_upload_init_invoke; optional_middlewares...)) + HTTP.register!(router, "GET", path_prefix * "/jobs/{job_id}/clusterinfo", OpenAPI.Servers.middleware(impl, get_clusterinfo_read, get_clusterinfo_validate, get_clusterinfo_invoke; optional_middlewares...)) + HTTP.register!(router, "GET", path_prefix * "/datasets/{dataset_id}/credentials", OpenAPI.Servers.middleware(impl, get_dataset_credentials_read, get_dataset_credentials_validate, get_dataset_credentials_invoke; optional_middlewares...)) + HTTP.register!(router, "GET", path_prefix * "/datasets", OpenAPI.Servers.middleware(impl, get_datasets_read, get_datasets_validate, get_datasets_invoke; optional_middlewares...)) + HTTP.register!(router, "GET", path_prefix * "/jobs/{job_id}/input", OpenAPI.Servers.middleware(impl, get_job_input_read, get_job_input_validate, get_job_input_invoke; optional_middlewares...)) + HTTP.register!(router, "GET", path_prefix * "/user/datasets", OpenAPI.Servers.middleware(impl, get_user_datasets_read, get_user_datasets_validate, get_user_datasets_invoke; optional_middlewares...)) + HTTP.register!(router, "POST", path_prefix * "/jobs/{job_id}/distributed_status", OpenAPI.Servers.middleware(impl, update_job_distributed_status_read, update_job_distributed_status_validate, update_job_distributed_status_invoke; optional_middlewares...)) + HTTP.register!(router, "POST", path_prefix * "/jobs/{job_id}/status", OpenAPI.Servers.middleware(impl, update_job_status_read, update_job_status_validate, update_job_status_invoke; optional_middlewares...)) + return router +end diff --git a/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl b/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl index 861be59..2701f42 100644 --- a/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl +++ b/test/server/openapigenerator_petstore_v3/petstore/src/apis/api_PetApi.jl @@ -47,7 +47,7 @@ function delete_pet_read(handler) openapi_params = Dict{String,Any}() path_params = HTTP.getparams(req) openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - headers = Dict{String,String}(HTTP.headers(req)) + headers = Dict{String,String}(req.headers) openapi_params["api_key"] = OpenAPI.Servers.to_param(String, headers, "api_key", ) req.context[:openapi_params] = openapi_params diff --git a/test/server/petstore_v2/petstore/src/apis/api_PetApi.jl b/test/server/petstore_v2/petstore/src/apis/api_PetApi.jl index 8a8eec7..9bccaf3 100644 --- a/test/server/petstore_v2/petstore/src/apis/api_PetApi.jl +++ b/test/server/petstore_v2/petstore/src/apis/api_PetApi.jl @@ -47,7 +47,7 @@ function delete_pet_read(handler) openapi_params = Dict{String,Any}() path_params = HTTP.getparams(req) openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - headers = Dict{String,String}(HTTP.headers(req)) + headers = Dict{String,String}(req.headers) openapi_params["api_key"] = OpenAPI.Servers.to_param(String, headers, "api_key", ) req.context[:openapi_params] = openapi_params diff --git a/test/server/petstore_v3/petstore/src/apis/api_PetApi.jl b/test/server/petstore_v3/petstore/src/apis/api_PetApi.jl index 8f42cf3..f3e3346 100644 --- a/test/server/petstore_v3/petstore/src/apis/api_PetApi.jl +++ b/test/server/petstore_v3/petstore/src/apis/api_PetApi.jl @@ -47,7 +47,7 @@ function delete_pet_read(handler) openapi_params = Dict{String,Any}() path_params = HTTP.getparams(req) openapi_params["petId"] = OpenAPI.Servers.to_param(Int64, path_params, "petId", required=true, ) - headers = Dict{String,String}(HTTP.headers(req)) + headers = Dict{String,String}(req.headers) openapi_params["api_key"] = OpenAPI.Servers.to_param(String, headers, "api_key", ) req.context[:openapi_params] = openapi_params From c3a9a98b5c0c8481ab98c0f675faee387fb5f3a1 Mon Sep 17 00:00:00 2001 From: tan Date: Tue, 23 Jun 2026 11:23:01 +0530 Subject: [PATCH 6/9] Make allany verbose tests HTTP 2 compatible Two HTTP.jl 2.x behavior changes broke test_debug: 1. verbose=true output: 1.x (and the curl backend) write the raw exchange to stderr; 2.x writes a '[http] ... via h1' format to stdout. The test captured stderr and asserted 'HTTP/1.1 200 OK', so under 2.x the pipe got no data and readavailable() blocked forever (hung the whole suite). Capture the right stream per version, assert the matching format, and close the pipe write-end before reading so an empty stream yields EOF instead of hanging. 2. response body framing: 1.x servers chunk the body ('27\r\n{json}\r\n0...'), 2.x sends it unframed with Content-Length. The custom-verbose test parsed the JSON via split(str,'\n')[2] (assuming a chunk-size line) -> BoundsError under 2.x. Extract the JSON object by brace span instead, framing-agnostic. --- test/client/allany/runtests.jl | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/test/client/allany/runtests.jl b/test/client/allany/runtests.jl index 374d257..bd8cd88 100644 --- a/test/client/allany/runtests.jl +++ b/test/client/allany/runtests.jl @@ -86,14 +86,26 @@ function test_debug(httplib::Symbol) ) api = M.DefaultApi(client) + # HTTP.jl 2.x routes `verbose=true` output to stdout in a "[http] ... via h1" + # format, whereas 1.x (and the Downloads/curl backend) write the raw exchange to + # stderr. Capture the right stream and assert on the matching format per version. + use_stdout = httplib === :http && OpenAPI.Clients._HTTP_V2 pipe = Pipe() - redirect_stderr(pipe) do + redirect = use_stdout ? redirect_stdout : redirect_stderr + redirect(pipe) do pet = M.AnyOfMappedPets(mapped_cat) api_return, http_resp = echo_anyof_mapped_pets_post(api, pet) @test pet_equals(api_return, pet) end - out_str = String(readavailable(pipe)) - @test occursin("HTTP/1.1 200 OK", out_str) + # Close the write end so the read sees EOF; without this, `readavailable` blocks + # forever when nothing was written to the captured stream. + close(pipe.in) + out_str = read(pipe, String) + if use_stdout + @test occursin("[http]", out_str) && occursin("200", out_str) + else + @test occursin("HTTP/1.1 200 OK", out_str) + end end if httplib === :downloads @@ -150,7 +162,10 @@ function test_debug(httplib::Symbol) write(iob, message) end data_in_str = String(take!(iob)) - data_in_str = strip(split(data_in_str, "\n")[2]) + # The curl backend reports the raw response body. HTTP.jl 1.x servers send it + # chunk-framed ("27\r\n{json}\r\n0\r\n\r\n"); 2.x sends an unframed body with a + # Content-Length. Extract the JSON object itself so the parse works either way. + data_in_str = data_in_str[findfirst('{', data_in_str):findlast('}', data_in_str)] data_in_json = JSON.parse(data_in_str) @test data_in_json["pet_type"] == "cat" @test data_in_json["hunts"] == true From 5dee6c667171bfa1a4ecf63c04c275fb277de06a Mon Sep 17 00:00:00 2001 From: tan Date: Tue, 23 Jun 2026 12:09:45 +0530 Subject: [PATCH 7/9] Resolve server header params case-insensitively for HTTP.jl 2.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HTTP.jl 2.x canonicalizes incoming request header field names (e.g. "api_key" arrives as "Api_key"), whereas 1.x preserves the sent case. The server's get_param did a case-sensitive Dict lookup, so header params silently resolved to nothing under HTTP 2.x — a required header param would 400, an optional one would be dropped. Add a case-insensitive fallback in get_param (src/server.jl) that fires only when the exact lookup misses. Header field names are case- insensitive per RFC, so this is spec-correct and version-agnostic; it also fixes already-generated servers without regeneration. Query/path param dicts are not canonicalized, so exact match always wins for them. Add regression tests in test/param_deserialize.jl covering canonicalized key resolution, exact-match precedence, missing/required params, and the to_param end-to-end path. --- src/server.jl | 14 ++++++++++++++ test/param_deserialize.jl | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/server.jl b/src/server.jl index 22e2e31..4029555 100644 --- a/src/server.jl +++ b/src/server.jl @@ -61,6 +61,20 @@ end function get_param(source::Dict, name::String, required::Bool) val = get(source, name, nothing) + if isnothing(val) + # HTTP header field names are case-insensitive, and some HTTP.jl versions + # canonicalize incoming request header names (e.g. "api_key" -> "Api_key"). + # Fall back to a case-insensitive match so header params resolve regardless + # of the HTTP.jl version. The exact lookup above wins first, so query/path + # params (whose dicts are not canonicalized) are unaffected. + lname = lowercase(name) + for (k, v) in source + if lowercase(k) == lname + val = v + break + end + end + end if required && isnothing(val) throw(ValidationException("required parameter \"$name\" missing")) end diff --git a/test/param_deserialize.jl b/test/param_deserialize.jl index 1881c71..2a9b7a6 100644 --- a/test/param_deserialize.jl +++ b/test/param_deserialize.jl @@ -1,7 +1,38 @@ using Test -using OpenAPI.Servers: deep_dict_repr -using OpenAPI: deep_object_to_array +using OpenAPI.Servers: deep_dict_repr, get_param, to_param +using OpenAPI: deep_object_to_array, ValidationException + +@testset "Case-insensitive header param lookup" begin + # HTTP.jl 2.x canonicalizes incoming request header names (e.g. the wire header + # "api_key" arrives as "Api_key"), while 1.x preserves the sent case. Header field + # names are case-insensitive per RFC, so server param lookup must resolve them + # regardless of the HTTP.jl version. See get_param in src/server.jl. + canonicalized = Dict{String,String}("Api_key" => "secret", "Uuid_parameter" => "abc") + + @testset "get_param resolves canonicalized keys" begin + @test get_param(canonicalized, "api_key", false) == "secret" + @test get_param(canonicalized, "uuid_parameter", false) == "abc" + # required param present only under its canonicalized key must not throw + @test get_param(canonicalized, "api_key", true) == "secret" + end + + @testset "exact match still wins" begin + # an exact key takes precedence over any case-insensitive fallback + mixed = Dict{String,String}("api_key" => "exact", "Api_key" => "canon") + @test get_param(mixed, "api_key", false) == "exact" + end + + @testset "genuinely missing param" begin + @test get_param(canonicalized, "missing", false) === nothing + @test_throws ValidationException get_param(canonicalized, "missing", true) + end + + @testset "to_param end-to-end (as generated code calls it)" begin + @test to_param(String, canonicalized, "api_key") == "secret" + @test to_param(String, canonicalized, "uuid_parameter"; required=true) == "abc" + end +end @testset "Test deep_dict_repr" begin @testset "Single level object" begin query_string = Dict("key1" => "value1", "key2" => "value2") From 1826f5a9c57cd947671d2096972cfceeb21472bf Mon Sep 17 00:00:00 2001 From: tan Date: Tue, 23 Jun 2026 12:24:59 +0530 Subject: [PATCH 8/9] Raise wait_server timeout to 90s for slow CI server startup Server processes launched via run_server use --pkgimages=no, forcing a full recompile on each launch. Under HTTP.jl 2.x (slower precompile) on cold CI runners this regularly exceeds the 20s wait, so the server tests time out and get skipped, and timed-out-but-still-launching processes orphan the port for the next testset. Bump the wait to 90s. --- test/testutils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/testutils.jl b/test/testutils.jl index f3c1993..161d790 100644 --- a/test/testutils.jl +++ b/test/testutils.jl @@ -27,7 +27,7 @@ end function wait_server(port) @info("Waiting for server", port) - is_ok = timedwait(20.0; pollint=2.0) do + is_ok = timedwait(90.0; pollint=2.0) do try resp = HTTP.request("GET", "http://127.0.0.1:$port/ping") return resp.status == 200 From bda15084082556e4e05f3a14d527cb0bda91d97e Mon Sep 17 00:00:00 2001 From: tan Date: Tue, 23 Jun 2026 12:55:32 +0530 Subject: [PATCH 9/9] bump minor version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index fad5b3e..91767da 100644 --- a/Project.toml +++ b/Project.toml @@ -4,7 +4,7 @@ keywords = ["Swagger", "OpenAPI", "REST"] license = "MIT" desc = "OpenAPI server and client helper for Julia" authors = ["JuliaHub Inc."] -version = "0.2.2" +version = "0.3.0" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"