Skip to content

REQUEST_ATTEMPT_LIMIT is ignored when fetch throws, so network errors are never retried #3148

Open

Description

@Stovedal

New Issue Checklist

Issue Description

REQUEST_ATTEMPT_LIMIT has no effect when fetch throws, which covers the most common transient failures in a Node service: connection reset, socket closed by the peer, connect timeout, DNS failure. Those requests are rejected after a single attempt.

The retry branch is only reachable when fetch returned a response:

https://github.com/parse-community/Parse-SDK-JS/blob/alpha/src/RESTController.ts#L219-L233

} else if (status >= 500 || status === 0) {
  // retry on 5XX or library error
  ...
  if (!isStream && ++attempts < CoreManager.get('REQUEST_ATTEMPT_LIMIT')) {

When fetch throws, control goes to the catch instead, which rejects with no retry:

https://github.com/parse-community/Parse-SDK-JS/blob/alpha/src/RESTController.ts#L237-L245

Two things makes me think this is a regression from the XHR to fetch migration:

1. status === 0 is unreachable under fetch. A Response always carries a status in 200–599; status 0 only exists for opaque responses, which Node never produces. Under XMLHttpRequest, status === 0 used to signal a network error, which is probably why the retry was expected to cover. The comment on L220, "retry on 5XX or library error", still describes that behaviour.

2. The existing tests simulate that impossible condition with a mock, so they pass while the path that runs is broken. In src/__tests__/RESTController-test.js:

it('retries on connection failure', async () => {
  mockFetch([{ status: 0 }, { status: 0 }, { status: 0 }, { status: 0 }, { status: 0 }])
  await expect(RESTController.ajax('POST', 'users', {})).rejects.toEqual(
    'Unable to connect to the Parse API'
  );
  expect(fetch.mock.calls.length).toBe(5);
});

mockFetch resolves a response with status: 0. Real fetch rejects instead, so this test exercises only the dead branch. returns a connection error on network failure has the same shape, and asserts the XMLHttpRequest failed: "Unable to connect to the Parse API" message that only that branch can produce.

Also, ECONNREFUSED is rejected as a bare string on L241. handleError then finds no .code, .error or .message to read, falls through to its second branch, and produces ParseError(100, 'XMLHttpRequest failed: "Unable to connect to the Parse API"') — so the underlying ECONNREFUSED is lost and the caller gets XHR wording.

Steps to reproduce

A local HTTP server accepts the connection and destroys the socket, which makes fetch throw TypeError: fetch failed.

const http = require('http')
const Parse = require('parse/node')

const run = async () => {
  let attempts = 0
  const server = http.createServer((req) => {
    attempts++
    req.socket.destroy()
  })
  await new Promise((r) => server.listen(0, '127.0.0.1', r))

  Parse.initialize('appId', null, 'masterKey')
  Parse.serverURL = `http://127.0.0.1:${server.address().port}/parse`
  Parse.CoreManager.set('REQUEST_ATTEMPT_LIMIT', 5)

  try {
    await new Parse.Query('Foo').first({ useMasterKey: true })
  } catch (e) {
    console.log('REQUEST_ATTEMPT_LIMIT :', Parse.CoreManager.get('REQUEST_ATTEMPT_LIMIT'))
    console.log('actual HTTP attempts  :', attempts)
  }
  server.close()
  process.exit(0)
}
run()

Actual Outcome

REQUEST_ATTEMPT_LIMIT : 5
actual HTTP attempts  : 1

Expected Outcome

A thrown network error should be eligible for the same retry policy as a 5xx response, up to REQUEST_ATTEMPT_LIMIT, since by definition no response was received:

REQUEST_ATTEMPT_LIMIT : 5
actual HTTP attempts  : 5

Caveat: retrying a non-idempotent request after a throw is not unconditionally safe, because the request may have been received but unanswered. We handle that by gating only on tunneled GET requests and it recovers effectively.

Environment

Server

  • Parse Server version: 9.9.0
  • Operating system: Ubuntu 26.04 (Heroku-26 stack), Node.js 24.16.0
  • Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): Heroku

Database

  • System (MongoDB or Postgres): MongoDB
  • Database version: n/a — database involved
  • Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): MongoDB Atlas

Client

  • Parse JS SDK version: 8.6.2

Activity

  1. parse-github-assistant commented on Oct 15, 2026

    @parse-github-assistant

    🚀 Thanks for opening this issue!

    ℹ️ You can help us to fix this issue faster by opening a pull request with a failing test. See our Contribution Guide for how to make a pull request, or read our New Contributor's Guide if this is your first time contributing.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions