Skip to content

feat(fetch): add result-based SafeFetch wrapper [Codex] - #367

Open
jderochervlk wants to merge 6 commits into
mainfrom
feat/safe-fetch
Open

jderochervlk wants to merge 6 commits into
mainfrom
feat/safe-fetch

Conversation

@jderochervlk

@jderochervlk jderochervlk commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #363

Raw Fetch rejects on request failure but fulfills with non-OK HTTP responses, so callers must handle both paths separately. The opt-in WebAPI.SafeFetch turns request and status failures into result values and preserves the response and rejection cause.

Without SafeFetch:

try {
  let response = await WebAPI.Fetch.fetch(url)
  if (!response.ok) {
    handleHttpError(response.status)
  } else {
    let body = await response->WebAPI.Response.json
    handleBody(body)
  }
} catch {
| cause => handleRequestError(cause)
}

With SafeFetch:

switch await WebAPI.SafeFetch.fetch(url) {
| Error(WebAPI.SafeFetch.FetchRejected(cause)) => handleRequestError(cause)
| Error(WebAPI.SafeFetch.ResponseNotOk({response})) =>
  handleHttpError(response.status)
| Ok(response) =>
  switch await response->WebAPI.SafeFetch.json {
  | Error({response, cause}) => handleBodyError(response.status, cause)
  | Ok({body}) => handleBody(body)
  }
}

Safe body readers return the body with its consumed response; json returns JSON.t, leaving application-specific validation to the caller.

Emitted JavaScript

fetch and fetchWithRequest construct their final result directly and call native fetch. The URL implementation is named fetchUrl and the public fetch alias is declared after both native calls, avoiding shadowing the browser's fetch without changing the raw bindings.

Actual output from the locked ReScript 13.0.0-alpha.5 compiler (URL implementation; body readers and the Request implementation omitted):

import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.mjs";

async function fetchUrl(url, init) {
  try {
    let response = await (
      init !== undefined ? fetch(url, init) : fetch(url)
    );
    if (response.ok) {
      return {
        TAG: "Ok",
        _0: response
      };
    } else {
      return {
        TAG: "Error",
        _0: {
          TAG: "ResponseNotOk",
          _0: {
            response: response
          }
        }
      };
    }
  } catch (raw_cause) {
    let cause = Primitive_exceptions.internalToException(raw_cause);
    return {
      TAG: "Error",
      _0: {
        TAG: "FetchRejected",
        _0: cause
      }
    };
  }
}

let fetch$1 = fetchUrl;
export { fetch$1 as fetch };

A supplied initializer is forwarded, and an absent initializer is omitted. The only runtime import is exception normalization, which preserves the existing exn contract and original rejection cause.

Fetch commonly rejects with a JavaScript Error or DOMException, but AbortSignal.abort(reason) can supply any JavaScript value. JsExn.fromException retrieves that original value from the exn wrapper. Native Fetch tests cover both the default AbortError and a custom string abort reason.

With esbuild 0.28.2 (bundle: true, minify: true, format: "esm", no gzip), an entry exporting only fetch shrinks from 641 to 358 bytes (44%); an entry exporting all SafeFetch helpers shrinks from 1,338 to 1,133 bytes. Both measurements include their bundled runtime dependencies and use the same compiler and bundler settings.

Validation: runtime suite (including both fetch entry points, optional initialization, non-OK responses, asynchronous rejection, and synchronous throws), all 27 public feature builds, formatting checks, and documentation build.

@jderochervlk

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-13T14:38:27.643183Z 518c165 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment thread src/fetch/Fetch.res Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 34977ccebd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jderochervlk
jderochervlk marked this pull request as ready for review October 13, 2026 13:27
Comment thread src/fetch/SafeFetch.res Outdated
@fhammerschmidt

Copy link
Copy Markdown
Member

Also maybe add some examples that use let?

@tsnobip tsnobip left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are a few open questions we should likely settle here before we generalize this kind of helper modules. Please chime in @fhammerschmidt @jderochervlk and anybody who'd like to! :)

Comment thread src/fetch/SafeFetch.res
Comment on lines +5 to +7
type fetchError =
| FetchRejected(exn)
| ResponseNotOk(httpError)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep absolutely.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely polyvariants.

Comment thread src/fetch/SafeFetch.res
body: 'body,
}

let checkOk = (response: Response.t): result<Response.t, httpError> =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

Comment thread rescript.json
"Request",
"Response"
"Response",
"SafeFetch"

Copy link
Copy Markdown
Member

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?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an opt-in SafeFetch wrapper for result-based Fetch workflows

4 participants