Generic Oauth2 authentication, enforce token refresh based on expiration value instead of http response code

The idea is:

Some business systems and services that are not natively supported by out of the box integrations require the use of the http request node with various generic authentication profiles (header, query, Oauth2, etc.). Sadly, some of them like Adobe Marketo Engage are not responding in standard ways when a token is expired. Example, when the Marketo access token is expired, you get an http 200 response with the error message in the body. It’s not giving a 401 / 403 or other standard http responses, meaning we have to parse the response to handle error situations. The positive aspect is that the Oauth2 token authorization process does return a standard response with an expiration. What I would suggest is a toggle to enforce the use of the expiration (in seconds) instead of relying on the http response code only.

My use case:

Greater flexibility in supporting non-standard api endpoints. There is no good workaround at the moment as if we want to keep the credentials secure, we can’t set a manual workflow to manage the token as the credentials will be in clear text in those specific workflow (security issue).

I think it would be beneficial to add this because:

It would expand n8n’s support for even more 3rd party integrations that are not natively supported.

Any resources to support this?

{
“access_token”: “cdf01657-110d-4155-99a7-f986b2ff13a0:int”,
“token_type”: “bearer”,
“expires_in”: 3599,
“scope”: “apis@acmeinc.com
}

Are you willing to work on this?

Yes but I’m not set to contribute via github…

The use case with Marketo is a real pain - a non-standard 200 + error body is much harder to catch than a 401. In the meantime, you can work around it by storing the token and its expires_in value from the OAuth response in workflow static data, then checking Date.now() >= expiryTimestamp before each request and manually triggering a refresh via HTTP Request if needed. Not ideal, but it keeps things predictable without waiting for the 200 error to surface.

Jay’s workaround gets you moving, but it breaks the part Pier-André is trying to protect: the refresh logic leaves the credential boundary and starts living in workflow state.

The cleaner feature shape is to store expires_at with the OAuth2 credential, refresh before the request when it is inside a small buffer, and only use the HTTP response as a fallback. Pier-André, for Marketo is the expired-token body always the 601/602 style response, or does it vary by endpoint?

Here is what I can see in an expired response.
This currently means I have to touch the credentials and workflow nodes each time I want to use this, or each hour, as N8N is not handling the token refresh at all as the response is still a 200).
There are a few documented error codes (I don’t know if this link will work for you without an Adobe login)

@oimrqs_ops If the link doesn’t work, here is the important stuff.

Error Codes

Last update: May 13, 2026

CREATED FOR:

  • Admin

Below are lists of REST API error codes, and an explanation of how errors are returned back to applications.

Handling and Logging Exceptions

When developing for Marketo, it is important that requests and responses get logged when an unexpected exception is encountered. While certain types of exceptions, such as expired authentication, can be safely handled by re-authentication, others may require support interactions, and requests and responses will always be requested in this scenario.

Error Types

The Marketo REST API can return three different types of errors under normal operation:

  • HTTP-Level: These errors are indicated by a 4xx code.
  • Response-Level: These errors are included in the “errors” array of the JSON response.
  • Record-Level: These errors are included in the “result” array of the JSON response, and are indicated on an individual record basis with the “status” field and “reasons” array.

For Response-Level and Record-Level error types, an HTTP status code of 200 is returned. For all error types, the HTTP reason phrase should not be evaluated as it is optional and subject to change.

HTTP-Level errors

Under normal operating circumstances Marketo should only return two HTTP status code errors, 413 Request Entity Too Large, and 414 Request URI Too Long. These are both recoverable through catching the error, modifying the request and retrying, but with smart coding practices, you should never encounter these in the wild.

Marketo will return 413 if the Request Payload exceeds 1MB, or 10MB in the case of Import Lead. In most scenarios it is unlikely to hit these limits, but adding a check to the size of the request and moving any records, which cause the limit to be exceeded to a new request should prevent any circumstances, which lead to this error being returned by any endpoints.

414 will be returned when the URI of a GET request exceeds 8KB. To avoid it, check against the length of your query string to see if it exceeds this limit. If it does change your request to a POST method, then input your query string as the request body with the additional parameter _method=GET. This forgoes the limitation on URIs. It is rare to hit this limit in most cases, but it is somewhat common when retrieving large batches of records with long individual filter values such as a GUID.
The Identity endpoint can return a 401 Unauthorized error. This is typically due to an invalid Client Id or invalid Client Secret. HTTP-Level Error Codes

That extract is enough, thanks. The key bit is that Marketo puts response-level errors inside a 200, so n8n cannot treat this like a normal 401 retry; the credential has to refresh from its own expiry clock before the HTTP node fires.

One small thing: in your failed call, is the expired-token response always the same Marketo code, or have you seen it change by endpoint?

It’s a bit hard to say. There are a decent list of error codes in the error response in addition to the 200 http response.
Those I have seen the most are 500, 601, 602, 604, 606, 607, 608.

Full list below…

Response Code Description Comment
500 Internal Server error The server encountered an unexpected condition that prevented it from fulfilling the request. Within Marketo, this may include improperly formed REST API request URLs.
502 Bad Gateway The remote server returned an error. Likely a timeout. The request should be retried with exponential backoff.
601* Access token invalid An Access Token parameter was included in the request, but the value was not a valid access token.
602* Access token expired The Access Token included in the call is no longer valid due to expiration.
603 Access denied Authentication is successful but the user does not have sufficient permission to call this API. [Additional permissions](custom-services.md) may need to be assigned to the user role, or Allowlist for IP-Based API Access may be enabled.
604* Request time-out The request was running for too long (for example, encountered database contention), or exceeded the time-out period specified in the header of the call.
605* HTTP Method not supported GET is not supported for the Sync Leads endpoint. POST must be used.
606 Max rate limit `%s`; exceeded with in `%s` secs The number of calls in the past 20 seconds was greater than 100
607 Daily quota reached The number of calls today exceeded the subscription’s quota (resets daily at 12:00AM CST).>Your quota can be found in your Admin->Web Services menu. You can increase your quota through your account manager.
608* API Temporarily Unavailable
609 Invalid JSON The body included in the request is not valid JSON.
610 Requested resource not found The URI in the call did not match a REST API resource type. This is often due to an incorrectly spelled or incorrectly formatted request URI
611* System error All unhandled exceptions
612 Invalid Content Type If you see this error, add a content type header specifying JSON format to your request. For example, try using `content type: application/json`. See this StackOverflow question for more details.
613 Invalid Multipart Request The multipart content of the POST was not formatted correctly
614 Invalid Subscription The destination subscription cannot be found or is unreachable. This usually indicates temporary inaccessibility.
615 Concurrent access limit reached At most, requests are processed by any subscription 10 at a time. This is returned if there are already 10 ongoing requests.
616 Invalid subscription type The appropriate Marketo subscription type is required to access the Custom Object Metadata API. Consult your CSM for details.
701 %s cannot be blank The reported field must not be empty in the request
702 No data found for a given search scenario No records matched the given search parameters. Note: Many failed search operations return `success = true` and no errors and set a warnings informational string.
703 The feature is not enabled for the subscription A beta feature that has not been in enabled in a user’s subscription
704 Invalid date format

That helps. The important split is that 602 is the clean expired-token case. 601 can justify one refresh attempt, but 500/604/606/607 are server, timeout, rate-limit or quota problems and should stay out of auth refresh.

So the feature request is tighter if it stays narrow: refresh from the OAuth credential expiry before the HTTP Request node runs, then leave Marketo’s response-body codes as fallback/error handling.

1 curtida

Yes, I think this is the cleanest way to define the request. Have a way to prioritize the token expiry timing for the refresh mechanism before the http request is made, then fallback on parsing of the http call response for error codes in header and body.

Hi, since this is a core component of n8n, do you think there is a less likely chance of this feature request making it into the code?

Form a quick search with cursor, I got the following findings.

  • Token refresh is reactive only: triggered when the HTTP response status matches tokenExpiredStatusCode (default 401).

  • n8n recently merged PR #26641, exposing tokenExpiredStatusCode on the generic OAuth2 credential UI — this helps APIs that return 403, but does not help Marketo, which returns 200 with response-level error codes like 602.

  • [ClientOAuth2Token](https://github.com/n8n-io/n8n/blob/master/packages/@n8n/client-oauth2/src/client-oauth2-token.ts) already parses expires_in and has an expired() method, but requestOAuth2 never calls it before making requests.

Should I open an issue on Github? [n8n-io/n8n](https://github.com/n8n-io/n8n/issues/new) linking to this? Seeing some related issues #17450, #16857 and #18517 (same expires_in theme, different APIs).

I’m not sure I have the skills to code something and submit a PR.

Feature implementation (section below from Cursor in Planning mode)

Target files

Credential UI

[packages/nodes-base/credentials/OAuth2Api.credentials.ts]( n8n/packages/nodes-base/credentials/OAuth2Api.credentials.ts at master · n8n-io/n8n · GitHub )

Change: Add toggle, e.g. refreshBeforeExpiry (default false for backward compatibility). Optional: expiry buffer in seconds (e.g. 60s before expiry).

Types

[packages/@n8n/client-oauth2/src/types.ts]( n8n/packages/@n8n/client-oauth2/src/types.ts at master · n8n-io/n8n · GitHub )

Change: Extend OAuth2CredentialData with new fields.

Core logic

[packages/core/.../request-helpers/oauth.ts]( n8n/packages/core/src/execution-engine/node-execution-context/utils/request-helpers/oauth.ts at master · n8n-io/n8n · GitHub )

Change: Before signing/sending the request, if toggle is on and token is near expiry, call existing refreshOrFetchToken().

Critical implementation detail:

ClientOAuth2Token.expired() computes expiry as now + expires_in in its constructor. When reloading a token from stored oauthTokenData, raw expires_in alone resets the clock on every request. You must persist an absolute timestamp when saving tokens:

  • On token acquisition/refresh in refreshOrFetchToken() and initial client-credentials fetch, store e.g. expires_at: Date.now() + expires_in * 1000 (or obtained_at + expires_in) inside oauthTokenData.

  • Update ClientOAuth2Token.expired() (or a helper) to prefer expires_at when present, falling back to expires_in only for freshly issued tokens.

  • If expires_in / expires_at is missing, skip proactive refresh and keep current HTTP-status fallback behavior.

Filed as GitHub issue #32423 (OAuth2 credentials not refreshing when API returns HTTP 200 on expired tokens). Happy to work on a PR if the team confirms scope.