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]
When
fetch()rejects (network error, CORS failure on the actual request, proxy or extension block),Client.request()instatic/app/api.tsxswallows the rejection. None ofsuccess,error, orcompleterun, sorequestPromise()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
http.clientspan forPUT /api/0/organizations/{org}/detectors/{detector_id}/hasspan.status: errorand nohttp.response.status_code, after ~133ms.OPTIONS) succeeded; the browser rejected the actualPUT.Root cause
request()passes a no-op as the rejection handler:requestPromise()only settles through thesuccess/errorcallbacks, so it stays pending.useUpdateDetector(static/app/views/detectors/hooks/index.ts) never reachesonError.useEditDetectorFormSubmit,await updateDetector(...)never returns, so itscatch(toast +onSubmitError) never runs andformModelstays in the saving state..catchonly 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, orfetchMutationcan hang the same way.Expected
A rejected fetch (other than
AbortError) should reach the error path and rejectrequestPromisewith aRequestError(status 0, noresponseJSON), so callers show their generic error toast and clear loading state, the same as for a 403 today.Proposed fix
ResponseMetaand callerrorHandler, thencompleteHandler. SkipAbortErrorso intentional cancellations stay silent.Out of scope
The environment-specific cause of the blocked request is being investigated separately.
Related: #116236 (broader
requestPromiseerror-handling refactor).--
View Junior Session [Sentry]