#885·ky

Feature: Support Modifying Options on Retry

Author: jdanilCreated Sep 13, 2026Updated Sep 14, 2026

Problem

The beforeRetry hook has the following description...

This hook enables you to modify the request right before retry.

... but it cannot modify request options. This is because the hook receives a frozen object.

Note, the current behaviour is a bit inconsistent, as Object.freeze is shallow. So options.retry.limit can be mutated for instance, but not options.onUploadProgress.

Context

My main motivation for this feature is to work around #739. In my case, some users are behind middleboxes that don't support HTTP/2. I would prefer not to degrade the experience for all users (by removing onUploadProgress completely), and only fallback where necessary.

Ideally if options could be modified, I could do something like this...

import ky, { isNetworkError } from "ky";

const DEFAULT_RETRY_METHODS = ["delete", "get", "head", "options", "put", "trace"];
const UPLOAD_FALLBACK_METHODS = ["patch", "post"];

const api = ky.create({
  hooks: {
    beforeRequest: [
      ({ options }) => {
        if (!options.onUploadProgress || options.body instanceof ReadableStream) {
          return;
        }

        const state = { transmitted: false };
        const { onUploadProgress } = options;

        options.context.uploadFallback = state;
        options.onUploadProgress = (progress, chunk) => {
          state.transmitted = true;
          onUploadProgress(progress, chunk);
        };
      },
    ],
    beforeRetry: [
      ({ error, options, request }) => {
        const state = options.context.uploadFallback;

        if (state && options.onUploadProgress) {
          if (!isNetworkError(error) || state.transmitted) {
            throw error;
          }

          options.onUploadProgress = undefined;

          return;
        }

        if (UPLOAD_FALLBACK_METHODS.includes(request.method.toLowerCase())) {
          throw error;
        }
      },
    ],
  },
  retry: {
    methods: [...DEFAULT_RETRY_METHODS, ...UPLOAD_FALLBACK_METHODS],
  },
});

I currently use a Proxy that wraps ky to achieve a similar outcome, but its quite complex.

Proposal

In #runBeforeRequestHooks and #retryFromError, pass the live options to the hooks, as we do with request...

this.#cachedNormalizedOptions = undefined;

hookResult = await this.#raceWithTotalTimeout(async () => hook({
  request: this.request,
  options: this.#options as unknown as NormalizedOptions,
  error,
  retryCount: this.#retryCount + 1,
}));

This would allow the hook to adjust the options ky uses for the next attempt.

I'd be happy to contribute a PR if this change would be something you'd like to support.

Alternatives

Another option could be for ky to support this fallback behaviour internally, but I figured it would probably be useful for options to be modifiable on retry like the rest of the request.