diff --git a/__tests__/Fetch_vitest.res b/__tests__/Fetch_vitest.res new file mode 100644 index 0000000..004509f --- /dev/null +++ b/__tests__/Fetch_vitest.res @@ -0,0 +1,26 @@ +open Vitest + +describe("Fetch.Response", () => { + open Expect + + test("make sets status + headers; body may be null", () => { + let headers = Fetch.Headers.make() + headers->Fetch.Headers.set("Location", "/somewhere") + headers->Fetch.Headers.set("Cache-Control", "public, max-age=3600") + let res = Fetch.Response.make(Nullable.null, {status: 307, headers}) + ( + res.status, + res.headers->Fetch.Headers.get("Location"), + res.headers->Fetch.Headers.get("Cache-Control"), + ) + ->expect + ->toEqual((307, Some("/somewhere"), Some("public, max-age=3600"))) + }) + + test("redirect sets Location + status", () => { + let res = Fetch.Response.redirect("https://example.com/y", 307) + (res.status, res.headers->Fetch.Headers.get("Location")) + ->expect + ->toEqual((307, Some("https://example.com/y"))) + }) +}) diff --git a/src/Fetch.res b/src/Fetch.res index f67acaf..9df3548 100644 --- a/src/Fetch.res +++ b/src/Fetch.res @@ -20,14 +20,31 @@ module Headers = { // missing forEach, getSetCookie } -module FetchResponse = { +// The web `Response` — one Web API type, so one binding for both its *read* +// side (what `fetch` resolves to) and its *construct* side (what an App Router +// route handler returns). The latter is why `make` / `redirect` live here: +// `next/navigation`'s `redirect` / `notFound` are control-flow throwers for +// pages (no custom headers; `notFound` renders the HTML page), and +// `NextServer.NextResponse`'s status is a closed 200/400/403/404/500 variant. +module Response = { type t = { ok: bool, status: int, + headers: Headers.t, arrayBuffer: unit => promise, text: unit => promise, json: unit => promise, } + + // Construct: `new Response(body, init)`. Body may be null (redirects, empty + // bodies); every `init` field is optional. + type init = {status?: int, statusText?: string, headers?: Headers.t} + @new external make: (Nullable.t, init) => t = "Response" + + // Static `Response.redirect(url, status)`. Sets Location + status only; for + // extra headers (e.g. `Cache-Control`) use `make` with `init.headers`. + @val @scope("Response") external redirect: (string, int) => t = "redirect" + let jsonResult = async response => { let text = await response.text() try JSON.parseOrThrow(text)->Result.Ok catch { @@ -43,7 +60,10 @@ module FetchResponse = { let json = async response => await response.json() } -@val external fetch: (string, {..}) => promise = "fetch" +// Former name; kept so existing consumers keep compiling. Prefer `Response`. +module FetchResponse = Response + +@val external fetch: (string, {..}) => promise = "fetch" let fetchJson = async (url: string, body: JSON.t) => { let options = {