Skip to content

feat(server): conditional token refresh#5516

Open
hassan254-prog wants to merge 2 commits intomasterfrom
wari/conditional-refresh-token
Open

feat(server): conditional token refresh#5516
hassan254-prog wants to merge 2 commits intomasterfrom
wari/conditional-refresh-token

Conversation

@hassan254-prog
Copy link
Contributor

@hassan254-prog hassan254-prog commented Feb 24, 2026

Describe the problem and your solution

  • Conditional token refresh allows tokens to be refreshed on demand, i.e., when a specific error is encountered. This can be defined in the proxy configuration or in the provider's configuration, ensuring that the next call is retried after the token is refreshed.

Add conditional refresh-token retries in proxy flow

This PR introduces configurable conditional token refresh for proxy requests, allowing retries to trigger a credential refresh based on specific HTTP status codes. The new refreshTokenOn option flows from provider metadata and SDK configuration through proxy config building, retry logic, and the server controller to call credential refresh before retrying, with expanded unit tests and documentation updates.

Key Changes

• Added refreshTokenOn support to proxy configuration types and builder in packages/types/lib/proxy/api.ts and packages/shared/lib/services/proxy/utils.ts
• Implemented refresh-triggered retries with onRefreshToken, MAX_REFRESH_TOKEN_ATTEMPTS, and retry budget adjustments in packages/shared/lib/services/proxy/request.ts
• Updated proxy controller to parse refresh-token-on header and invoke refreshOrTestCredentials before retrying in packages/server/lib/controllers/proxy/allProxy.ts
• Centralized refreshable auth modes in packages/shared/lib/constants.ts and used them in packages/shared/lib/services/connections/credentials/refresh.ts and packages/shared/lib/services/proxy/retry.ts
• Added tests for refresh-triggered retries and caps in packages/shared/lib/services/proxy/request.unit.test.ts and packages/shared/lib/services/proxy/retry.unit.test.ts, plus docs updates in docs/spec.yaml, docs/reference/functions.mdx, and docs/reference/sdks/node.mdx

Possible Issues

• Refresh failures from refreshOrTestCredentials are currently ignored in packages/server/lib/controllers/proxy/allProxy.ts, potentially causing wasted retries with stale credentials.
• Documentation grammar for “status codes that triggers” appears in multiple docs sections and may need correction.


This summary was automatically generated by @propel-code-bot

propel-code-bot[bot]

This comment was marked as outdated.

propel-code-bot[bot]

This comment was marked as outdated.

propel-code-bot[bot]

This comment was marked as outdated.

@hassan254-prog hassan254-prog force-pushed the wari/conditional-refresh-token branch from 8342f6f to dadd707 Compare February 25, 2026 13:12
Copy link
Contributor

@propel-code-bot propel-code-bot bot left a comment

Choose a reason for hiding this comment

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

No issues were identified; the conditional token refresh implementation appears solid and ready to merge.

Status: No Issues Found | Risk: Low

Review Details

📁 15 files reviewed | 💬 0 comments

Instruction Files
├── .claude/
│   ├── agents/
│   │   └── nango-docs-migrator.md
│   └── skills/
│       ├── agent-builder-skill/
│       │   ├── EXAMPLES.md
│       │   └── SKILL.md
│       ├── creating-integration-docs/
│       │   └── SKILL.md
│       └── creating-skills-skill/
│           └── SKILL.md
├── AGENTS.md
└── GEMINI.md


import { networkError } from '@nangohq/utils';

import { REFRESHABLE_AUTH_MODES } from '../../constants.js';
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need to have a .js suffix? Also, once we are in double .. territory, it's worth just importing it with an absolute path

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it does, when using node16 module resolution, relative imports must include the file extension, using an absolute path would mean we will need to add the package shared as its own dependency.

let refreshTokenAttempts = 0;

const minRetriesForRefresh = this.config.refreshTokenOn?.length && this.onRefreshToken ? MAX_REFRESH_TOKEN_ATTEMPTS + 1 : 0;
const maxRetries = Math.max(this.config.retries ?? 0, minRetriesForRefresh);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain this block? Why are we assigning a MAX+1 to a variable with minimum in the name, and then assigning a min to a max again? They all have retry and/or refresh in the name, it's confusing :/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’ve updated this with clearer naming. The +1 ensures there are enough attempt slots to handle the two token refreshes.

@linear
Copy link

linear bot commented Feb 26, 2026

@TBonnin
Copy link
Collaborator

TBonnin commented Feb 27, 2026

@hassan254-prog I haven't follow the discussion about this feature. Can you explain why do we need it?

@hassan254-prog
Copy link
Contributor Author

@hassan254-prog I haven't follow the discussion about this feature. Can you explain why do we need it?

We are addressing a performance issue with salesforce-related provider's proxy calls. Currently, every time we call getConnection to retrieve tokens for proxy calls, we perform an introspection call to salesforce to check if the token is still valid. This adds unnecessary overhead.
With the new approach, we only refresh the token when a specific error code is encountered, rather than on every call. The pr to change salesforce configuration will come after this is merged.

Copy link
Contributor

@marcindobry marcindobry left a comment

Choose a reason for hiding this comment

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

LGTM but this seems like it could break something by accident and would love someone with more knowledge about the code to also have a look. I haven't worked with the proxy and retry logic yet

@TBonnin
Copy link
Collaborator

TBonnin commented Mar 2, 2026

@hassan254-prog I haven't follow the discussion about this feature. Can you explain why do we need it?

We are addressing a performance issue with salesforce-related provider's proxy calls. Currently, every time we call getConnection to retrieve tokens for proxy calls, we perform an introspection call to salesforce to check if the token is still valid. This adds unnecessary overhead. With the new approach, we only refresh the token when a specific error code is encountered, rather than on every call. The pr to change salesforce configuration will come after this is merged.

why are we always introspecting and not checking the credentials.expires_at we have stored in the db?

@hassan254-prog
Copy link
Contributor Author

@hassan254-prog I haven't follow the discussion about this feature. Can you explain why do we need it?

We are addressing a performance issue with salesforce-related provider's proxy calls. Currently, every time we call getConnection to retrieve tokens for proxy calls, we perform an introspection call to salesforce to check if the token is still valid. This adds unnecessary overhead. With the new approach, we only refresh the token when a specific error code is encountered, rather than on every call. The pr to change salesforce configuration will come after this is merged.

why are we always introspecting and not checking the credentials.expires_at we have stored in the db?

No, we don’t have that info stored for salesforce, they don't return it. Their token also doesn’t include expiry details, since it includes opaque strings and not a regular jwt, which is why we need to introspect it to determine whether it has expired.

@TBonnin
Copy link
Collaborator

TBonnin commented Mar 3, 2026

No, we don’t have that info stored for salesforce, they don't return it. Their token also doesn’t include expiry details, since it includes opaque strings and not a regular jwt, which is why we need to introspect it to determine whether it has expired.

I can see plenty of salesforce connections with a set credentials_expires_at? What are those?

@hassan254-prog
Copy link
Contributor Author

hassan254-prog commented Mar 3, 2026

From our logic, when we upsert an Oauth2 connection, the getExpiresAtFromCredentials defaults to now + 1d if there is no expires_at value in the credentials.

Can we set the real expiresAt when introspecting the token so we can rely on it to decide when to refresh or not?

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.

3 participants