Skip to content

Conversation

@jannikschaper
Copy link

Fixes #1613

I followed @brendandburns comments from the linked issue:

I think it is totally fine to add an optional typed parameter to the makeApiClient signature. Thanks for checking!

And I'm pretty sure that it will work. That's the Configuration object that flows down through to fetch ultimately (if I tracked the code right

@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented May 12, 2025

CLA Signed

The committers listed above are authorized under a signed CLA.

@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: jannikschaper
Once this PR has been reviewed and has the lgtm label, please assign mstruebing for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. label May 12, 2025
@k8s-ci-robot
Copy link
Contributor

Welcome @jannikschaper!

It looks like this is your first PR to kubernetes-client/javascript 🎉. Please refer to our pull request process documentation to help your PR have a smooth ride to approval.

You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. Here is the bot commands documentation.

You can also check if kubernetes-client/javascript has its own contribution guidelines.

You may want to refer to our testing guide if you run into trouble with your tests not passing.

If you are having difficulty getting your pull request seen, please follow the recommended escalation practices. Also, for tips and tricks in the contribution process you may want to read the Kubernetes contributor cheat sheet. We want to make sure your contribution gets all the attention it needs!

Thank you, and welcome to Kubernetes. 😃

@k8s-ci-robot k8s-ci-robot added size/M Denotes a PR that changes 30-99 lines, ignoring generated files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. and removed cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. labels May 12, 2025

const resultPromise = fetch(request.getUrl(), {
method: method,
body: body as any,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
body: body as any,
body: body as BodyInit,

If we do this typecast we should at least cast into the correct type.

Copy link
Author

Choose a reason for hiding this comment

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

Could do that. Would lose the pure symmetry with the original code then. If you're aware that I deliberately copied the send implementation verbatim, trying to stay close to the original code, but still prefer to have the more correct type cast, I can do that!

binary: () => resp.buffer(),
};
return new ResponseContext(resp.status, headers, body);
});
Copy link
Member

Choose a reason for hiding this comment

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

Not 100% sure but do we need a catch here as well or will this be catched by the client code?

Copy link
Author

Choose a reason for hiding this comment

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

This code, as is (without a catch) should behave identically to the status quo. I copied the send implementation verbatim from ./gen/http/isomorphic-fetch.ts (as noted above); only adding the timeout: options?.timeoutMs, line. Since the original code didn't need a catch, I argue this code must not need it either

// This HTTP API implementation is a copy of ./gen/http/isomorphic-fetch.ts,
// extended with the timeout pass-through code.
httpApi: {
send(request: RequestContext): Observable<ResponseContext> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't love pulling this full implementation into this library. Is there an easier way to inject the timeout?

Copy link
Author

@jannikschaper jannikschaper May 19, 2025

Choose a reason for hiding this comment

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

I thought that too, it would be much nicer to inject the extra field without having to rip out the entire function. But I believe there's no other way. The auto-generated code passes the configuration object (without any timeout value, not even passed through) directly into fetch. Not even a dependency-injected fetch or anything that might be overwriteable, but a fetch directly imported from node-fetch.

Expand code screenshot

Original code screenshot

Maybe it's still possible to reach into the code via JavaScript black magic to overwrite the fetch implementation and amend the timeout parameter, but I have a hunch such code will be even uglier than copying the 22 lines of code

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about changing the code generator itself? They're usually pretty good about merging changes quickly into the generator.

Copy link
Author

Choose a reason for hiding this comment

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

I wanted to avoid that seeming rabbit hole. But maybe it is a better solution, I will look into it

@davidgamero
Copy link
Contributor

i'm testing out a PR on the generator for passing through the signal property on the RequestContext type

middleware has access to the RequestContext which would allow adding a middleware that sets the abortsignal

another alternative is to add a signal input to the Configuration Type to expose it directly in the options. this would be a more accessible call, but it would add more complexity

OpenAPITools/openapi-generator#21323

@davidgamero
Copy link
Contributor

@jannikschaper how do you feel about the middleware injection approach?

something like this:

/**
 * Middleware that adds an abort signal to the request context.
 * This can be used to abort requests using an AbortController.
 * @param signal AbortSignal to use for the request
 * @returns Middleware that sets the signal in the request context
 */
function abortSignalMiddleware(signal: AbortSignal): Middleware {
  return {
    pre: async (c: RequestContext) => {
      c.setSignal(signal)
      return c
    },
    post: async (c: ResponseContext) => {
      return c
    },
  }
}

@k8s-triage-robot
Copy link

The Kubernetes project currently lacks enough contributors to adequately respond to all PRs.

This bot triages PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the PR is closed

You can:

  • Mark this PR as fresh with /remove-lifecycle stale
  • Close this PR with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle stale

@k8s-ci-robot k8s-ci-robot added the lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. label Aug 27, 2025
@jannikschaper
Copy link
Author

It works!

const makeApiClient = <T extends ApiType>(
    kubeConfig: KubeConfig,
    ApiClientType: ApiConstructor<T>,
    requestTimeoutInSeconds: number,
): T => {
    // Copied from KubeConfig.makeApiClient and adapted to add timeout
    // Suggested by @kubernetes/client-node maintainer (https://github.com/kubernetes-client/javascript/pull/2430#issuecomment-2918109624)

    const cluster = kubeConfig.getCurrentCluster();
    if (!cluster) {
        throw new Error('No active cluster!');
    }

    return new ApiClientType(createConfiguration({
        baseServer: new ServerConfiguration(cluster.server, {}),
        authMethods: {
            default: kubeConfig,
        },
        middleware: [{
            pre: (context) => {
                const controller = new AbortController();
                setTimeout(() => controller.abort(), requestTimeoutInSeconds * 1000);
                context.setSignal(controller.signal);

                return new Observable(Promise.resolve(context));
            },
            post: (context) => new Observable(Promise.resolve(context)),
        }],
    }));
};

Thank you so much @davidgamero for going out of your way and getting PRs merged both here and even in OpenAPI to fix my use case. Using a signal is better than a hardcoded timeout value, because you can implement a timeout and more on top of it, good idea. Thanks!

@jannikschaper jannikschaper deleted the timeout-ms branch October 30, 2025 11:26
@jannikschaper
Copy link
Author

@davidgamero I have a follow-up question to the timeout approach you suggested, if you don't mind

How to clean up dangling timeout instances when requests complete normally? For example, with a 120s timeout and on average 10 successful Kubernetes API requests per second, there would be over a thousand timeout promises still hanging around in the NodeJS event loop.

Each of them also eventually calling .abort() on the request, which is additional mental load to make sure these delayed false-positive aborts don't cause troubles.

A naive implementation might look like this:

function timeoutMiddleware(milliseconds: number): Middleware {
    let timeoutId: NodeJS.Timeout;

    return {
        pre: async (c: RequestContext) => {
            const abortController = new AbortController();
            timeoutId = setTimeout(() => abortController.abort(), milliseconds);
            c.setSignal(abortController.signal);

            return c;
        },
        post: async (c: ResponseContext) => {
            clearTimeout(timeoutId);

            return c;
        },
    };
}

But this is wrong, because parallel requests would overwrite each other's timeoutId. You'd need to attach the timeoutId to the request context, but as far as I can tell, there is no way to inject a value into a RequestContext and get it out reliably in the ResponseContext. The only possibility I can think of is to inject a cookie with a timeout ID in the request and hope that the Kubernetes API server returns the cookie in its response. But this is obviously a terrible solution.

Do you have an idea how to solve the problem of dangling timeouts with this library? Is there simply no way, and the best solution is to just deal with dangling timeouts aborting each not-anymore-existent request long after it finished?

Thank you for your help!

@DrauzJu
Copy link

DrauzJu commented Nov 4, 2025

@jannikschaper We have found a solution for that:

promiseMiddleware: [{
    pre: async (context) => {
        context.setSignal(AbortSignal.timeout(requestTimeoutInSeconds * 1000));

        return context;
    },
    post: async (context) => context,
}],

This way, we don't have to cleanup the timeout ourself because NodeJs internally calls Timeout.unref() to prevent the process from exiting if there is still such a pending timeout: https://github.com/nodejs/node/blob/3a33ee3cf09a36f6c2456b053efa8d7f20728956/lib/internal/abort_controller.js#L139-L161

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

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

not possible to set a client timeout

7 participants