Repository navigation
feat(fetch): add result-based SafeFetch wrapper [Codex] #367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
34977cc
3cceaa8
eb25639
fd61238
2ccb973
518c165
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -330,7 +330,8 @@ | |
| "Headers", | ||
| "HeadersInit", | ||
| "Request", | ||
| "Response" | ||
| "Response", | ||
| "SafeFetch" | ||
| ] | ||
| }, | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| type httpError = { | ||
| response: Response.t, | ||
| } | ||
|
|
||
| type fetchError = | ||
| | FetchRejected(exn) | ||
| | ResponseNotOk(httpError) | ||
|
Comment on lines
+5
to
+7
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if we should not use polymorphic variants here, this way we could more easily compose errors, what do you think @jderochervlk @fhammerschmidt ?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep absolutely.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely polyvariants. |
||
|
|
||
| type readError = { | ||
| response: Response.t, | ||
| cause: exn, | ||
| } | ||
|
|
||
| type response<'body> = { | ||
| response: Response.t, | ||
| body: 'body, | ||
| } | ||
|
|
||
| let checkOk = (response: Response.t): result<Response.t, httpError> => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpicking but LLMs tend to annotate functions a lot, which is not very idiomatic in rescript, this kind of code will likely be taken as a reference at some point, maybe we should remove these type annotations? |
||
| if response.ok { | ||
| Ok(response) | ||
| } else { | ||
| Error({response: response}) | ||
| } | ||
|
|
||
| let fetchUrl = async (url: string, ~init: option<Request.requestInit>=?) => { | ||
| try { | ||
| let response = await ( | ||
| switch init { | ||
| | Some(init) => Fetch.fetch(url, ~init) | ||
| | None => Fetch.fetch(url) | ||
| } | ||
| ) | ||
| if response.ok { | ||
| Ok(response) | ||
| } else { | ||
| Error(ResponseNotOk({response: response})) | ||
| } | ||
| } catch { | ||
| | cause => Error(FetchRejected(cause)) | ||
| } | ||
| } | ||
|
|
||
| let fetchWithRequest = async (request: Request.t, ~init: option<Request.requestInit>=?) => { | ||
| try { | ||
| let response = await ( | ||
| switch init { | ||
| | Some(init) => Fetch.fetchWithRequest(request, ~init) | ||
| | None => Fetch.fetchWithRequest(request) | ||
| } | ||
| ) | ||
| if response.ok { | ||
| Ok(response) | ||
| } else { | ||
| Error(ResponseNotOk({response: response})) | ||
| } | ||
| } catch { | ||
| | cause => Error(FetchRejected(cause)) | ||
| } | ||
| } | ||
|
|
||
| // Bind the public name after both native calls so it cannot shadow JavaScript fetch. | ||
| let fetch = fetchUrl | ||
|
|
||
| let read = async (response: Response.t, readBody: Response.t => promise<'body>) => { | ||
| try { | ||
| Ok({response, body: await readBody(response)}) | ||
| } catch { | ||
| | cause => Error({response, cause}) | ||
| } | ||
| } | ||
|
|
||
| let arrayBuffer = response => read(response, Response.arrayBuffer) | ||
| let blob = response => read(response, Response.blob) | ||
| let bytes = response => read(response, Response.bytes) | ||
| let formData = response => read(response, Response.formData) | ||
| let json = response => read(response, Response.json) | ||
| let text = response => read(response, Response.text) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| /** | ||
| Describes a response whose `ok` field is `false`. | ||
|
|
||
| The original response is retained so callers can inspect its status, headers, and body. | ||
| */ | ||
| type httpError = { | ||
| response: Response.t, | ||
| } | ||
|
|
||
| /** | ||
| Describes an expected failure while producing or classifying a response. | ||
|
|
||
| - `FetchRejected(cause)` means the native Fetch operation threw or its promise rejected. Fetch does | ||
| not expose a reliable browser-independent distinction between network, abort, permission, and | ||
| other request failures. The `exn` preserves the original JavaScript rejection value; use | ||
| `JsExn.fromException` to retrieve it. A custom abort reason can be any JavaScript value, so the | ||
| cause is not restricted to `JsError.t`. | ||
| - `ResponseNotOk({response})` means Fetch fulfilled with a response whose `ok` field is `false`. | ||
| The response remains available for inspection and body reading. | ||
| */ | ||
| type fetchError = | ||
| | FetchRejected(exn) | ||
| | ResponseNotOk(httpError) | ||
|
|
||
| /** | ||
| Describes a rejected response body reader. | ||
|
|
||
| The record retains both the response whose body was being consumed and the original rejection | ||
| cause. | ||
| */ | ||
| type readError = { | ||
| response: Response.t, | ||
| cause: exn, | ||
| } | ||
|
|
||
| /** | ||
| A successfully read body paired with its original response. | ||
|
|
||
| The response body has been consumed. Clone the response before reading when another consumer also | ||
| needs the body. | ||
| */ | ||
| type response<'body> = { | ||
| response: Response.t, | ||
| body: 'body, | ||
| } | ||
|
|
||
| /** | ||
| Classifies an existing response using its native `ok` field. | ||
|
|
||
| Returns `Ok(response)` for successful responses and `Error({response})` otherwise. This function | ||
| does not read the body and is useful with responses obtained from the raw `Fetch` API. | ||
| */ | ||
| let checkOk: Response.t => result<Response.t, httpError> | ||
|
|
||
| /** | ||
| Fetches a URL and returns expected request and HTTP status failures as values. | ||
|
|
||
| The returned promise fulfills with `Error(FetchRejected(cause))` when the native operation throws or | ||
| rejects, `Error(ResponseNotOk({response}))` when the response is not OK, or `Ok(response)` for an OK | ||
| response. | ||
| */ | ||
| let fetch: (string, ~init: Request.requestInit=?) => promise<result<Response.t, fetchError>> | ||
|
|
||
| /** | ||
| Fetches an existing `Request.t` and returns expected request and HTTP status failures as values. | ||
|
|
||
| This has the same result contract as `fetch` and accepts an optional request initializer that is | ||
| passed to the native Fetch operation. | ||
| */ | ||
| let fetchWithRequest: ( | ||
| Request.t, | ||
| ~init: Request.requestInit=?, | ||
| ) => promise<result<Response.t, fetchError>> | ||
|
|
||
| /** | ||
| Reads the response body as an `ArrayBuffer.t`. | ||
|
|
||
| Returns the body with the original response, or a `readError` when the native reader rejects. | ||
| */ | ||
| let arrayBuffer: Response.t => promise<result<response<ArrayBuffer.t>, readError>> | ||
|
|
||
| /** | ||
| Reads the response body as a `Blob.t`. | ||
|
|
||
| Returns the body with the original response, or a `readError` when the native reader rejects. | ||
| */ | ||
| let blob: Response.t => promise<result<response<Blob.t>, readError>> | ||
|
|
||
| /** | ||
| Reads the response body as an array of bytes. | ||
|
|
||
| Returns the body with the original response, or a `readError` when the native reader rejects. | ||
| */ | ||
| let bytes: Response.t => promise<result<response<array<int>>, readError>> | ||
|
|
||
| /** | ||
| Reads the response body as `FormData.t`. | ||
|
|
||
| Returns the body with the original response, or a `readError` when the native reader rejects. | ||
| */ | ||
| let formData: Response.t => promise<result<response<FormData.t>, readError>> | ||
|
|
||
| /** | ||
| Parses the response body as `JSON.t`. | ||
|
|
||
| Malformed JSON and other native reader rejections become `readError` values. This function does not | ||
| validate the parsed JSON against an application domain type. | ||
| */ | ||
| let json: Response.t => promise<result<response<JSON.t>, readError>> | ||
|
|
||
| /** | ||
| Reads the response body as a string. | ||
|
|
||
| Returns the body with the original response, or a `readError` when the native reader rejects. | ||
| */ | ||
| let text: Response.t => promise<result<response<string>, readError>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we maybe have the Safe modules as a distinct feature?