Skip to content

Rejected fetch in api.tsx leaves requestPromise pending #126120

Description

@sentry-junior

When fetch() rejects (network error, CORS failure on the actual request, proxy or extension block), Client.request() in static/app/api.tsx swallows the rejection. None of success, error, or complete run, so requestPromise() never settles and any UI awaiting it hangs.

Observed impact

On the monitor edit page (/monitors/:detectorId/edit/), clicking Save leaves the button spinning forever. No error toast is shown, the saving state is never cleared, and no Sentry error is captured.

Evidence

  • The frontend http.client span for PUT /api/0/organizations/{org}/detectors/{detector_id}/ has span.status: error and no http.response.status_code, after ~133ms.
  • No matching backend transaction exists in the same trace. Other API calls in that trace were sampled at 1.0, so the PUT never reached Django.
  • The CORS preflight (OPTIONS) succeeded; the browser rejected the actual PUT.
  • Reported by a single user; reproduces in Incognito with VPN off and works for other users. The trigger is likely environment-specific, but the hang affects every failure of this kind.

Root cause

request() passes a no-op as the rejection handler:

fetchRequest.then(
  async response => { ... },
  () => {
    // Ignore failed fetch calls or errors in the fetch request itself (e.g. cancelled requests)
  }
).catch(...)
  • requestPromise() only settles through the success / error callbacks, so it stays pending.
  • useUpdateDetector (static/app/views/detectors/hooks/index.ts) never reaches onError.
  • In useEditDetectorFormSubmit, await updateDetector(...) never returns, so its catch (toast + onSubmitError) never runs and formModel stays in the saving state.
  • The trailing .catch only sees errors thrown inside the success path, so the failure is never reported to Sentry.

This isn't detector-specific: any caller of requestPromise, useApi, or fetchMutation can hang the same way.

Expected

A rejected fetch (other than AbortError) should reach the error path and reject requestPromise with a RequestError (status 0, no responseJSON), so callers show their generic error toast and clear loading state, the same as for a 403 today.

Proposed fix

  • In the rejection handler, build a status-0 ResponseMeta and call errorHandler, then completeHandler. Skip AbortError so intentional cancellations stay silent.
  • Optionally capture the rejection in Sentry (sampled or fingerprinted) so these failures are visible.

Out of scope

The environment-specific cause of the blocked request is being investigated separately.

Related: #116236 (broader requestPromise error-handling refactor).

--

View Junior Session [Sentry]

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions