From 488c6a77a8635c198f35d413af693c8d8120409d Mon Sep 17 00:00:00 2001 From: oboard Date: Fri, 21 Aug 2026 16:33:57 +0800 Subject: [PATCH 1/2] feat: unify the HTTP request contract across backends The three HTTP backends (native, JS, mongoose C) previously produced materially different HttpRequest values for the same request, so app behavior depended on which backend a deployment used: - Query strings: native stripped ?query before dispatch (the handler could not recover it); JS routed against the raw req.url (path+query), so GET /hello?x=1 404'd a registered GET /hello route. - Bodies: native read bodies for POST/PUT/PATCH and framed requests; JS read a body only when the method was exactly POST, so PUT/PATCH reached handlers with an empty raw_body. - Headers: headers were a case-sensitive Map[StringView, StringView]. Node lowercases incoming keys while native keeps wire casing, so event.req.headers.get("X-Test") behaved differently per backend even though HTTP field names are case-insensitive. Additionally the JS adapter did its own inline dispatch instead of reusing the shared dispatch_http, so the JS serve path was not covered by the in-process dispatch tests. This change defines one backend-independent request contract: - Routing splits the request-target into a pure path and a query (query string retained separately on HttpRequest.query; the path is used for route matching and static asset resolution). - A shared request_has_body predicate drives body reading identically for both adapters (POST/PUT/PATCH always; or framed via content-length/transfer-encoding). - HttpRequest.headers and HttpResponse.headers are now owned, case-insensitive Map[CaseInsensitiveString, StringView]; the implicit String -> CaseInsensitiveString conversion keeps existing call sites (map literals, .get/.set/[...]) compiling unchanged while making lookups case-insensitive. - The JS adapter now funnels through the shared dispatch_http, so the JS serve path gets the same normalization, body reading, and error handling as native. - The mongoose C adapter builds a case-insensitive header map and inherits the query-splitting fix via dispatch_http. A request-conformance suite (request_conformance.mbt) runs on both the js and native targets and asserts: query strings do not break routing, path and query are exposed separately, fragments are stripped, PUT and PATCH bodies reach the handler, and request headers are case-insensitive. Verified: moon test passes on both targets, and a live smoke test drives the native and JS servers identically for query routing, PUT/ PATCH bodies, and mixed-case headers. Co-Authored-By: Claude Opus 4.8 (1M context) --- dispatch.mbt | 46 +++++++++++++++++-- mocket.js.mbt | 57 ++++++++--------------- mocket.native.mbt | 40 +++++----------- native/mongoose/mongoose.mbt | 6 +-- native/mongoose/moon.pkg | 3 +- request.mbt | 9 +++- request_conformance.mbt | 88 ++++++++++++++++++++++++++++++++++++ response.mbt | 5 +- 8 files changed, 177 insertions(+), 77 deletions(-) create mode 100644 request_conformance.mbt diff --git a/dispatch.mbt b/dispatch.mbt index e485f84..0285f51 100644 --- a/dispatch.mbt +++ b/dispatch.mbt @@ -1,17 +1,55 @@ +///| +/// Split a raw HTTP request-target into its path and query string. +/// +/// Routes match against the path; the query is retained separately on the +/// `HttpRequest`. A `#fragment` is never used for routing, so it is stripped +/// from the query rather than exposed. +fn split_request_target(target : String) -> (String, String) { + let (path, rest) = match target.find("?") { + Some(q) => (target[:q].to_owned(), target[q + 1:].to_owned()) + None => (target, "") + } + let query = match rest.find("#") { + Some(f) => rest[:f].to_owned() + None => rest + } + (path, query) +} + +///| +/// Whether a request carries a body: POST/PUT/PATCH always may, and any +/// method with an explicit (non-empty) `content-length` or a +/// `transfer-encoding` (i.e. chunked) frame is treated as having a body. +/// The same rule is used by every backend so bodies are read consistently. +fn request_has_body( + http_method : String, + headers : Map[@http.CaseInsensitiveString, StringView], +) -> Bool { + match http_method { + "POST" | "PUT" | "PATCH" => true + _ => + headers.get("transfer-encoding") is Some(_) || + headers.get("content-length") + .map(value => value.to_owned().trim() != "0") + .unwrap_or(false) + } +} + ///| pub async fn dispatch_http( mocket : Mocket, http_method : String, url : String, - headers : Map[StringView, StringView], + headers : Map[@http.CaseInsensitiveString, StringView], raw_body : Bytes, ) -> HttpResponse { - let (params, handler) = match mocket.find_route(http_method, url) { + let (path, query) = split_request_target(url) + let (params, handler) = match mocket.find_route(http_method, path) { Some((h, p)) => (p, h) _ => ({}, handle_not_found()) } let event = { - req: { http_method, url, raw_body, headers }, + req: { http_method, url: path, query, raw_body, headers }, res: HttpResponse::new(OK), params, } @@ -28,4 +66,4 @@ pub async fn dispatch_http( responder.output(buf) event.res.raw_body = buf.to_bytes() event.res -} +} \ No newline at end of file diff --git a/mocket.js.mbt b/mocket.js.mbt index 948e19a..5c4ad5c 100644 --- a/mocket.js.mbt +++ b/mocket.js.mbt @@ -307,8 +307,8 @@ fn listen_port(address : String) -> Int { pub fn listen_ffi(mocket : Mocket, address : String) -> Unit { let port = listen_port(address) let server = create_server(fn(req, res, _) { - // 优化:简化 headers 转换逻辑 - let string_headers : Map[StringView, StringView] = Map([]) + // 构造大小写不敏感的头部映射表(HTTP 字段名不区分大小写) + let string_headers : Map[@http.CaseInsensitiveString, StringView] = Map([]) let json_val = req.headers().to_value().to_json() catch { _ => { res.write_head(400, @js.Object::new().to_value()) @@ -322,34 +322,22 @@ pub fn listen_ffi(mocket : Mocket, address : String) -> Unit { return } - // 批量转换 headers,减少单个处理的开销 headers.each(fn(key, value) { if value is String(v) { string_headers.set(key, v.to_string_view()) } }) - let (params, handler) = match - mocket.find_route(req.req_method(), req.url()) { - Some((h, p)) => (p, h) - _ => ({}, handle_not_found()) - } - let event = { - req: { - http_method: req.req_method(), - url: req.url(), - headers: string_headers, - raw_body: b"", - }, - res: HttpResponse::new(OK), - params, - } + + let http_method = req.req_method() + let url = req.url() + let should_read_body = request_has_body(http_method, string_headers) async_run(() => { - // 如果是 post,先等待 data 事件 - if event.req.http_method == "POST" { + let mut raw = b"" + if should_read_body { let buffer = Buffer() let mut total_size = 0 let mut exceeded = false - suspend(fn(res, _) { + suspend(fn(done, _) { req.on("data", data => { if !exceeded { let chunk = node_body_chunk_to_bytes(data) @@ -361,7 +349,7 @@ pub fn listen_ffi(mocket : Mocket, address : String) -> Unit { } } }) - req.on("end", _ => res(())) + req.on("end", _ => done(())) }) catch { _ => () } @@ -370,35 +358,30 @@ pub fn listen_ffi(mocket : Mocket, address : String) -> Unit { res.end(@js.Value::cast_from("Request body too large")) return } - event.req.raw_body = buffer.to_bytes() + raw = buffer.to_bytes() } - // 执行中间件链和处理器 - let responder = mocket.execute_middlewares(event, handler) catch { - err => mocket.handle_request_error(event, err) + // 交给统一的 dispatch_http:路由、查询拆分、中间件与错误处理全部一致。 + let response = dispatch_http(mocket, http_method, url, string_headers, raw) catch { + _ => HttpResponse::new(InternalServerError).body("Internal Server Error") } - // let boundary = "----------------moonbit-" + port.to_string() - responder.options(event.res) res.write_head( - event.res.status_code.to_int(), + response.status_code.to_int(), { - let headers_obj = @js.Value::from_json(event.res.headers.to_json()) catch { + let mut headers_obj = @js.Value::from_json(response.headers.to_json()) catch { _ => @js.Object::new().to_value() } - if !event.res.cookies.is_empty() { - let cookies = event.res.cookies + if !response.cookies.is_empty() { + let cookies = response.cookies .values() - .map(fn(c) { Show::to_string(c) }) + .map(fn(cookie) { Show::to_string(cookie) }) .to_array() set_js_property(headers_obj, "Set-Cookie", array_to_js(cookies)) } headers_obj }, ) - let buf = Buffer() - responder.output(buf) - event.res.raw_body = buf.to_bytes() - res.end(@js.Value::cast_from(event.res.raw_body)) + res.end(@js.Value::cast_from(response.raw_body)) }) }) start_server(server, address, websocket_accept_key) diff --git a/mocket.native.mbt b/mocket.native.mbt index aa13027..ee37b44 100644 --- a/mocket.native.mbt +++ b/mocket.native.mbt @@ -62,18 +62,18 @@ fn request_method_to_string(meth : @http.RequestMethod) -> String { ///| fn string_headers_to_views( headers : Map[@http.CaseInsensitiveString, String], -) -> Map[StringView, StringView] { - let out : Map[StringView, StringView] = Map([]) - headers.each((key, value) => out.set(Show::to_string(key), value)) +) -> Map[@http.CaseInsensitiveString, StringView] { + let out : Map[@http.CaseInsensitiveString, StringView] = Map([]) + headers.each((key, value) => out.set(key, value)) out } ///| fn view_headers_to_strings( - headers : Map[StringView, StringView], + headers : Map[@http.CaseInsensitiveString, StringView], ) -> Map[@http.CaseInsensitiveString, String] { let out : Map[@http.CaseInsensitiveString, String] = Map([]) - headers.each((key, value) => out.set(key.to_owned(), value.to_owned())) + headers.each((key, value) => out.set(key, value.to_owned())) out } @@ -121,27 +121,6 @@ fn is_websocket_upgrade(request : @http.Request) -> Bool { header_equals(request.headers, "upgrade", "websocket") } -///| -fn request_has_body(request : @http.Request) -> Bool { - match request.meth { - Post | Put | Patch => true - _ => - request.headers.get("transfer-encoding") is Some(_) || - request.headers - .get("content-length") - .map(value => value.trim() != "0") - .unwrap_or(false) - } -} - -///| -fn request_route_path(path : String) -> String { - match path.find("?") { - Some(query_start) => path[:query_start].to_owned() - None => path - } -} - ///| fn find_ws_route( mocket : Mocket, @@ -374,6 +353,7 @@ async fn send_native_response( conn.end_response() } +///| ///| async fn handle_http_request( mocket : Mocket, @@ -381,7 +361,8 @@ async fn handle_http_request( body_reader : &@io.Reader, conn : @http.ServerConnection, ) -> Unit { - let raw_body = if request_has_body(request) { + let headers = string_headers_to_views(request.headers) + let raw_body = if request_has_body(request_method_to_string(request.meth), headers) { let content_length = request.headers .get("content-length") .map(s => @string.parse_int(s.trim()) catch { _ => 0 }) @@ -398,11 +379,12 @@ async fn handle_http_request( } else { b"" } + // `dispatch_http` normalizes `request.path` into a path + query internally. let response = dispatch_http( mocket, request_method_to_string(request.meth), - request_route_path(request.path), - string_headers_to_views(request.headers), + request.path, + headers, raw_body, ) catch { err => { diff --git a/native/mongoose/mongoose.mbt b/native/mongoose/mongoose.mbt index ef01a19..24b8035 100644 --- a/native/mongoose/mongoose.mbt +++ b/native/mongoose/mongoose.mbt @@ -118,8 +118,8 @@ fn from_cbytes(bytes : Bytes) -> String { } ///| -fn parse_headers(headers_text : String) -> Map[StringView, StringView] { - let headers : Map[StringView, StringView] = Map([]) +fn parse_headers(headers_text : String) -> Map[@http.CaseInsensitiveString, StringView] { + let headers : Map[@http.CaseInsensitiveString, StringView] = Map([]) headers_text .split("\n") .each(line => { @@ -129,7 +129,7 @@ fn parse_headers(headers_text : String) -> Map[StringView, StringView] { let key = line[:idx].trim() let value = line[idx + 1:].trim() if key != "" { - headers.set(key, value) + headers.set(key.to_owned(), value) } } } diff --git a/native/mongoose/moon.pkg b/native/mongoose/moon.pkg index 2c13fcf..11d4deb 100644 --- a/native/mongoose/moon.pkg +++ b/native/mongoose/moon.pkg @@ -1,5 +1,6 @@ import { "oboard/mocket", + "moonbitlang/async/http", "moonbitlang/core/encoding/utf8", "moonbitlang/core/string", } @@ -10,4 +11,4 @@ supported_targets = "+native" options( "native-stub": [ "mocket.stub.c", "mongoose.c" ], -) +) \ No newline at end of file diff --git a/request.mbt b/request.mbt index bfae822..b51d24e 100644 --- a/request.mbt +++ b/request.mbt @@ -1,8 +1,14 @@ ///| pub(all) struct HttpRequest { http_method : String + /// The request-target path, without query string or fragment. + /// Routing matches against this value. url : String - headers : Map[StringView, StringView] + /// The raw query string without the leading `?`; empty when absent. + /// Fragments (`#...`) are stripped. + query : String + /// Case-insensitive request headers (HTTP field names are case-insensitive). + headers : Map[@http.CaseInsensitiveString, StringView] mut raw_body : Bytes } @@ -67,6 +73,7 @@ test "read_body" { let req = HttpRequest::{ http_method: "POST", url: "/", + query: "", headers: Map([]), raw_body: b"{\"Hello\":\"World!\"}", } diff --git a/request_conformance.mbt b/request_conformance.mbt new file mode 100644 index 0000000..0a17261 --- /dev/null +++ b/request_conformance.mbt @@ -0,0 +1,88 @@ +///| +/// Request-contract conformance suite. +/// +/// These tests exercise the shared `dispatch_http` entry point, which every +/// backend (native, C/mongoose, JS) now funnels through, so the guarantees +/// below hold identically on the `+js` and `+native` targets (the workspace +/// runs the tests on both). + +async test "query string does not break path routing" { + let app = new() + app.get("/hello", _ => "hello") + let response = dispatch_http(app, "GET", "/hello?x=1", {}, b"") + @test.assert_eq(response.status_code.to_int(), 200) + let body : String = response.read_body() + @test.assert_eq(body, "hello") +} + +///| +async test "query string is exposed separately from the route path" { + let app = new() + let captured : Array[String] = [] + app.get("/hello/:name", event => { + captured.push(event.req.url) + captured.push(event.req.query) + "ok" + }) + ignore(dispatch_http(app, "GET", "/hello/moonbit?x=1&y=2", {}, b"")) + @test.assert_eq(captured, ["/hello/moonbit", "x=1&y=2"]) +} + +///| +async test "fragment is stripped from the query string" { + let app = new() + let captured : Array[String] = [] + app.get("/hello", event => { + captured.push(event.req.query) + "ok" + }) + ignore(dispatch_http(app, "GET", "/hello?a=1#frag", {}, b"")) + @test.assert_eq(captured, ["a=1"]) +} + +///| +async test "route with no query string yields an empty query" { + let app = new() + let captured : Array[String] = [] + app.get("/plain", event => { + captured.push(event.req.query) + "ok" + }) + ignore(dispatch_http(app, "GET", "/plain", {}, b"")) + @test.assert_eq(captured, [""]) +} + +///| +async test "PUT and PATCH bodies are delivered to the handler" { + let app = new() + let captured : Array[String] = [] + app.put("/raw1", event => { + let text : String = event.req.body() + captured.push(text) + "accepted" + }) + app.patch("/raw2", event => { + let text : String = event.req.body() + captured.push(text) + "accepted" + }) + ignore(dispatch_http(app, "PUT", "/raw1", {}, b"payload-put")) + ignore(dispatch_http(app, "PATCH", "/raw2", {}, b"payload-patch")) + @test.assert_eq(captured, ["payload-put", "payload-patch"]) +} + +///| +async test "request headers are case-insensitive" { + let app = new() + let captured : Array[StringView?] = [] + app.get("/hdr", event => { + captured.push(event.req.headers.get("X-Test")) + captured.push(event.req.headers.get("x-test")) + captured.push(event.req.headers.get("X-TEST")) + "ok" + }) + let headers : Map[@http.CaseInsensitiveString, StringView] = Map([]) + headers["X-Test"] = "1" + ignore(dispatch_http(app, "GET", "/hdr", headers, b"")) + @test.assert_eq(captured, [Some("1"), Some("1"), Some("1")]) +} \ No newline at end of file diff --git a/response.mbt b/response.mbt index 42c0412..75e757d 100644 --- a/response.mbt +++ b/response.mbt @@ -1,7 +1,7 @@ ///| pub(all) struct HttpResponse { mut status_code : StatusCode - headers : Map[StringView, StringView] + headers : Map[@http.CaseInsensitiveString, StringView] cookies : Map[String, CookieItem] mut raw_body : Bytes } @@ -11,6 +11,7 @@ pub fn[T : BodyReader] HttpResponse::read_body(self : HttpResponse) -> T raise { T::from_request({ http_method: "", url: "", + query: "", headers: self.headers, raw_body: self.raw_body, }) @@ -19,7 +20,7 @@ pub fn[T : BodyReader] HttpResponse::read_body(self : HttpResponse) -> T raise { ///| pub fn HttpResponse::new( status_code : StatusCode, - headers? : Map[StringView, StringView], + headers? : Map[@http.CaseInsensitiveString, StringView], cookies? : Map[String, CookieItem], raw_body? : Bytes, ) -> HttpResponse { From e8cd28a9fc1be08a4c2d1e4de00f18daa7746a49 Mon Sep 17 00:00:00 2001 From: oboard Date: Fri, 21 Aug 2026 16:44:26 +0800 Subject: [PATCH 2/2] style: run moon fmt and moon info Applies `moon fmt` reformatting to the changed sources and the responder tests, and `moon info` to regenerate pkg.generated.mbti for the updated public API (headers keyed by CaseInsensitiveString; the new HttpRequest.query field). Co-Authored-By: Claude Opus 4.8 (1M context) --- dispatch.mbt | 9 +++++---- mocket.js.mbt | 7 +++++-- mocket.native.mbt | 6 +++++- native/mongoose/mongoose.mbt | 4 +++- native/mongoose/moon.pkg | 2 +- pkg.generated.mbti | 10 ++++++---- request_conformance.mbt | 3 +-- responder_test.mbt | 18 +++++++----------- 8 files changed, 33 insertions(+), 26 deletions(-) diff --git a/dispatch.mbt b/dispatch.mbt index 0285f51..50e437e 100644 --- a/dispatch.mbt +++ b/dispatch.mbt @@ -29,9 +29,10 @@ fn request_has_body( "POST" | "PUT" | "PATCH" => true _ => headers.get("transfer-encoding") is Some(_) || - headers.get("content-length") - .map(value => value.to_owned().trim() != "0") - .unwrap_or(false) + headers + .get("content-length") + .map(value => value.to_owned().trim() != "0") + .unwrap_or(false) } } @@ -66,4 +67,4 @@ pub async fn dispatch_http( responder.output(buf) event.res.raw_body = buf.to_bytes() event.res -} \ No newline at end of file +} diff --git a/mocket.js.mbt b/mocket.js.mbt index 5c4ad5c..3f84508 100644 --- a/mocket.js.mbt +++ b/mocket.js.mbt @@ -362,8 +362,11 @@ pub fn listen_ffi(mocket : Mocket, address : String) -> Unit { } // 交给统一的 dispatch_http:路由、查询拆分、中间件与错误处理全部一致。 - let response = dispatch_http(mocket, http_method, url, string_headers, raw) catch { - _ => HttpResponse::new(InternalServerError).body("Internal Server Error") + let response = dispatch_http( + mocket, http_method, url, string_headers, raw, + ) catch { + _ => + HttpResponse::new(InternalServerError).body("Internal Server Error") } res.write_head( response.status_code.to_int(), diff --git a/mocket.native.mbt b/mocket.native.mbt index ee37b44..0721ed9 100644 --- a/mocket.native.mbt +++ b/mocket.native.mbt @@ -354,6 +354,7 @@ async fn send_native_response( } ///| + ///| async fn handle_http_request( mocket : Mocket, @@ -362,7 +363,10 @@ async fn handle_http_request( conn : @http.ServerConnection, ) -> Unit { let headers = string_headers_to_views(request.headers) - let raw_body = if request_has_body(request_method_to_string(request.meth), headers) { + let raw_body = if request_has_body( + request_method_to_string(request.meth), + headers, + ) { let content_length = request.headers .get("content-length") .map(s => @string.parse_int(s.trim()) catch { _ => 0 }) diff --git a/native/mongoose/mongoose.mbt b/native/mongoose/mongoose.mbt index 24b8035..e939d72 100644 --- a/native/mongoose/mongoose.mbt +++ b/native/mongoose/mongoose.mbt @@ -118,7 +118,9 @@ fn from_cbytes(bytes : Bytes) -> String { } ///| -fn parse_headers(headers_text : String) -> Map[@http.CaseInsensitiveString, StringView] { +fn parse_headers( + headers_text : String, +) -> Map[@http.CaseInsensitiveString, StringView] { let headers : Map[@http.CaseInsensitiveString, StringView] = Map([]) headers_text .split("\n") diff --git a/native/mongoose/moon.pkg b/native/mongoose/moon.pkg index 11d4deb..840f198 100644 --- a/native/mongoose/moon.pkg +++ b/native/mongoose/moon.pkg @@ -11,4 +11,4 @@ supported_targets = "+native" options( "native-stub": [ "mocket.stub.c", "mongoose.c" ], -) \ No newline at end of file +) diff --git a/pkg.generated.mbti b/pkg.generated.mbti index 5eaf240..4b23aec 100644 --- a/pkg.generated.mbti +++ b/pkg.generated.mbti @@ -2,6 +2,7 @@ package "oboard/mocket" import { + "moonbitlang/async/http", "moonbitlang/core/buffer", "moonbitlang/core/json", } @@ -11,7 +12,7 @@ pub fn __ws_emit(Bytes, Bytes, Bytes) -> Unit pub fn cookie_to_string(Array[CookieItem]) -> String -pub async fn dispatch_http(Mocket, String, String, Map[StringView, StringView], Bytes) -> HttpResponse +pub async fn dispatch_http(Mocket, String, String, Map[@http.CaseInsensitiveString, StringView], Bytes) -> HttpResponse pub fn dispatch_ws_event((WebSocketEvent) -> Unit, WebSocketPeer, String, Bytes) -> Unit @@ -91,7 +92,8 @@ pub impl Responder for Html pub(all) struct HttpRequest { http_method : String url : String - headers : Map[StringView, StringView] + query : String + headers : Map[@http.CaseInsensitiveString, StringView] mut raw_body : Bytes } pub fn[T : BodyReader] HttpRequest::body(Self) -> T raise @@ -101,14 +103,14 @@ pub impl Responder for HttpRequest pub(all) struct HttpResponse { mut status_code : StatusCode - headers : Map[StringView, StringView] + headers : Map[@http.CaseInsensitiveString, StringView] cookies : Map[String, CookieItem] mut raw_body : Bytes } pub fn HttpResponse::body(Self, &Responder) -> Self pub fn HttpResponse::delete_cookie(Self, String) -> Unit pub fn HttpResponse::json(Self, &ToJson) -> Self -pub fn HttpResponse::new(StatusCode, headers? : Map[StringView, StringView], cookies? : Map[String, CookieItem], raw_body? : Bytes) -> Self +pub fn HttpResponse::new(StatusCode, headers? : Map[@http.CaseInsensitiveString, StringView], cookies? : Map[String, CookieItem], raw_body? : Bytes) -> Self pub fn[T : BodyReader] HttpResponse::read_body(Self) -> T raise pub fn HttpResponse::set_cookie(Self, String, String, max_age? : Int, path? : String, domain? : String, secure? : Bool, http_only? : Bool, same_site? : SameSiteOption) -> Unit pub fn HttpResponse::to_responder(Self) -> &Responder diff --git a/request_conformance.mbt b/request_conformance.mbt index 0a17261..7d16331 100644 --- a/request_conformance.mbt +++ b/request_conformance.mbt @@ -5,7 +5,6 @@ /// backend (native, C/mongoose, JS) now funnels through, so the guarantees /// below hold identically on the `+js` and `+native` targets (the workspace /// runs the tests on both). - async test "query string does not break path routing" { let app = new() app.get("/hello", _ => "hello") @@ -85,4 +84,4 @@ async test "request headers are case-insensitive" { headers["X-Test"] = "1" ignore(dispatch_http(app, "GET", "/hdr", headers, b"")) @test.assert_eq(captured, [Some("1"), Some("1"), Some("1")]) -} \ No newline at end of file +} diff --git a/responder_test.mbt b/responder_test.mbt index 3e97096..8e8c833 100644 --- a/responder_test.mbt +++ b/responder_test.mbt @@ -75,9 +75,7 @@ async test "custom status html body infers html content type" { ///| async test "json response infers json content type" { let app = new() - app.get("/created", _ => { - HttpResponse::new(Created).json({ "ok": true }) - }) + app.get("/created", _ => HttpResponse::new(Created).json({ "ok": true })) let response = dispatch_http(app, "GET", "/created", {}, b"") inspect(response.status_code.to_int(), content="201") @test.assert_eq( @@ -92,16 +90,14 @@ async test "json response infers json content type" { async test "explicit content type wins over inferred body type" { let app = new() app.get("/custom-body", _ => { - HttpResponse::new( - OK, - headers={ "Content-Type": "application/vnd.custom" }, - ).body("plain text") + HttpResponse::new(OK, headers={ "Content-Type": "application/vnd.custom" }).body( + "plain text", + ) }) app.get("/custom-json", _ => { - HttpResponse::new( - OK, - headers={ "Content-Type": "application/vnd.custom+json" }, - ).json({ "ok": true }) + HttpResponse::new(OK, headers={ + "Content-Type": "application/vnd.custom+json", + }).json({ "ok": true }) }) let body_response = dispatch_http(app, "GET", "/custom-body", {}, b"")