Skip to content

[MAR-3084] Add fetch-with-retries package#4

Open
leafrogers wants to merge 7 commits into
mainfrom
mar-3084/add-fetch-with-retries-package
Open

[MAR-3084] Add fetch-with-retries package#4
leafrogers wants to merge 7 commits into
mainfrom
mar-3084/add-fetch-with-retries-package

Conversation

@leafrogers

@leafrogers leafrogers commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Following on from the prep PR, this one adds the first real shared package using the new workflow. This package can then be imported into our projects as @financial-times/martech-fetch-with-retries.

Note

All thoughts and ideas are very welcome for this PR too!

Sum’ry

This PR is split out into two commits:

  1. adding the fetch-with-retries package
  2. adding the release/publish workflow needed to get that package into Cloudsmith

The package is intended to replace local fetch-retry helpers that we currently maintain in a few different repos, so we can stop solving the same problem slightly differently in several places.

Why this is happening

Across the Martech estate, we already have a few variants of “fetch, but retry when upstream is flaky”. They’re not all identical, but they’re close enough that it seems worth consolidating. This package caters to all of those variants.

What changed

Shared package

Added @financial-times/martech-fetch-with-retries.

Usage:

const import { createFetchWithRetries } from '@financial-times/martech-fetch-with-retries';
const fetchWithRetries = createFetchWithRetries(logger);

const response = await fetchWithRetries('https://ft.com'); // tries up to 3 times if needed
// …

Default retry behaviour

By default, the package retries:

  • thrown fetch errors
  • HTTP statuses 408, 429, 500, 502, 503, 504

And unless configured otherwise, only retries these idempotent methods:

  • GET, HEAD, OPTIONS, PUT, DELETE

Retries for POST and PATCH are explicit opt-ins via:

  • retryPostRequests
  • retryPatchRequests

Configuration

The wrapper supports:

Configurable prop Type
maximumAttempts number
backoffStrategy function
retryPostRequests boolean
retryPatchRequests booleans

Logging

The package emits structured debug and warn retry events, and is intended to work with @dotcom-reliability-kit/logger.

Tests/docs

Included:

  • unit tests, end-to-end tests, a TypeScript smoke test, a package README and changelog

Release/publish plumbing

This PR also adds the minimum publishing setup so the package is actually consumable:

  • CircleCI publish workflow
  • Cloudsmith npm configuration
  • tag-based release trigger for this package

🌸 🐄 🐖 🐓 🐈 🐑 🌸 🐄 🐖 🐓 🐈 🐑 🌸 🐄 🐖 🐓 🐈 🐑 🌸 🐄 🐖 🐓 🐈 🐑 🌸

A bit o’ context

Key decisions made

Native Node fetch only

This package assumes globalThis.fetch and does not support custom fetch injection. That keeps the runtime and type surface smaller, and matches current usage in our repos.

Transport-level only

This package retries requests and logs retry events, but it doesn’t parse JSON or normalize responses. That felt like the right split. Retry logic is the shared concern here; response/body handling can stay separate.

Sensible defaults

Retrying POST and PATCH by default felt too risky for a shared package, and looking around our repos, not all of them need it.

No Retry-After support in v1

This was considered, and then removed as none of our existing repos use it.

It felt like edge-case handling that we don’t need at the moment. If we need more sophisticated delay behaviour later, we can revisit it or use backoffStrategy.

Publishing approach

This PR adds package publishing, but doesn’t introduce fully automated releasing, unlike @dotcom-reliability-kit. Chat with me about why I decided against this, if you’re intrigued!

So for now, the release model is intentionally simple:

  1. bump the package version
  2. update the changelog
  3. create a GitHub release with a matching tag like fetch-with-retries-v1.0.1
  4. let CircleCI publish that version to Cloudsmith

🌸 🐄 🐖 🐓 🐈 🐑 🌸 🐄 🐖 🐓 🐈 🐑 🌸 🐄 🐖 🐓 🐈 🐑 🌸 🐄 🐖 🐓 🐈 🐑 🌸

Next steps 🚀

When/if we’re happy with this package, we can start replacing retry helpers in a couple of repos. I’ve already tried it locally with ip-martech-scs-to-marketo and it seems good.

A moving depiction of this package

kKLmEOQy45G4QzFPOO

Add the first real shared package to this repo:
@financial-times/martech-fetch-with-retries.

This package is intentionally narrow. It is a wrapper around
native Node fetch: not a general HTTP client and not a JSON parsing helper.
It keeps fetch semantics by returning `Response`, retries thrown fetch errors and
selected HTTP statuses, and leaves response parsing to the consumer.

Key decisions in v1.0.0:
- standardise fetch usage and retries across Martech repos
- native fetch only; no custom fetch injection
- requirement to pass in a logger, which warns on retry
- only retries for idempotent methods by default
- explicit opt-in booleans for POST and PATCH retries
- explicit retry allowlist for 408, 429, 500, 502, 503 and 504
- no `Retry-After` support to keep the surface smaller, and none of our
  repos currently support it (https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After)
- best-effort cancellation of discarded responses before retrying

Includes unit, end-to-end and TypeScript smoke tests, plus package-local docs and
changelog. Replaces the temporary workspace smoke-test package now that the real
package provides the repo’s concrete example workspace.
Adds the release-only plumbing needed to publish the `fetch-with-retries` package
from CircleCI to FT’s private Cloudsmith npm registry. I’ve already
tested this with a pre-release (v0.1.0) and it worked fine, and have
imported it successfully into a local clone of one of our other repos,
for testing.

The publishing workflow is tag-gated, package-scoped, and publishes via
`npm publish --workspace packages/fetch-with-retries`.

This repo uses Cloudsmith rather than public npm because the package is intended
for internal use, and should not be published publicly.
@leafrogers leafrogers requested a review from a team as a code owner June 4, 2026 11:42

@saldixon saldixon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

review in progress, but thought I"d submit some questions so far.

Comment thread .circleci/config.yml
Comment thread packages/fetch-with-retries/types/internal.js
Comment thread packages/fetch-with-retries/lib/helpers.js
Comment thread packages/fetch-with-retries/lib/helpers.js Outdated
Comment thread packages/fetch-with-retries/lib/helpers.js
Base automatically changed from mar-3084/prep-repo-for-packages to main June 10, 2026 15:38
Comment thread packages/fetch-with-retries/test/end-to-end/index.spec.js

@saldixon saldixon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lovely stuff!

const POST_METHOD = 'POST';
const PATCH_METHOD = 'PATCH';
const RETRYABLE_STATUS_CODES = new Set([
HTTP_STATUS_CODES.REQUEST_TIMEOUT,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can’t believe I left this unalphabetised

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.

2 participants