Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(linear-retries): [nan-2309] handle linear case #3108

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

khaliqgant
Copy link
Member

@khaliqgant khaliqgant commented Dec 4, 2024

Linear sends back a 4xx status code with a RATELIMITED string in the body. Currently this doesn't get picked up by our retry logic. Searching using the headers will retry even if it isn't a strict 429.

NAN-2309

Test by mimicing a response that is like the linear response

{
  "request": {
    "headers": {
      "authorization": "REDACTED",
      "user-agent": "nango-node-client/0.45.1 (linux/6.8.0-1018-aws; node.js/20.12.2); sdk"
    },
    "method": "POST",
    "url": "https://api.linear.app/graphql"
  },
  "response": {
    "headers": {
      "x-ratelimit-complexity-remaining": "971913",
      "x-request-id": "8eca3a6e066a3076-SEA",
      "content-length": "535",
      "vary": "Accept-Encoding",
      "x-complexity": "520",
      "x-ratelimit-complexity-limit": "1000000",
      "x-ratelimit-requests-remaining": "0",
      "x-ratelimit-requests-reset": "1733302464386",
      "x-ratelimit-complexity-reset": "1733302464386",
      "content-type": "application/json; charset=utf-8",
      "connection": "close",
      "x-ratelimit-requests-limit": "1200"
    },
    "code": 400
  },
  "error": {
    "message": "Request failed with status code 400",
    "payload": {
      "safeHeaders": {
        "authorization": "REDACTED",
        "user-agent": "nango-node-client/0.45.1 (linux/6.8.0-1018-aws; node.js/20.12.2); sdk"
      },
      "code": "ERR_BAD_REQUEST",
      "data": {
        "errors": [
          {
            "extensions": {
              "userError": true,
              "code": "RATELIMITED",
              "meta": {},
              "type": "ratelimited",
              "userPresentableMessage": "Rate limit exceeded. Only 1200 requests are allowed per 1 hour. For more information see our developer docs at: https://developers.linear.app/docs/graphql/working-with-the-graphql-api/rate-limiting."
            },
            "message": "Rate limit exceeded. Only 1200 requests are allowed per 1 hour. For more information see our developer docs at: https://developers.linear.app/docs/graphql/working-with-the-graphql-api/rate-limiting"
          }
        ]
      },
      "method": "POST",
      "url": "https://api.linear.app/graphql"
    }
  }
}

Copy link

linear bot commented Dec 4, 2024

@khaliqgant khaliqgant requested a review from a team December 4, 2024 09:55
Copy link
Collaborator

@bodinsamuel bodinsamuel left a comment

Choose a reason for hiding this comment

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

If we indeed have headers I would suggest something else

"x-complexity": "520",
      "x-ratelimit-complexity-limit": "1000000",
      "x-ratelimit-requests-remaining": "0",
      "x-ratelimit-requests-reset": "1733302464386",
      "x-ratelimit-complexity-reset": "1733302464386",

We should change the if in retry to search for headers whatever the status code

packages/shared/lib/services/proxy.service.ts Outdated Show resolved Hide resolved
scripts/validation/providers/schema.json Outdated Show resolved Hide resolved
…of github.com:NangoHQ/nango into khaliq/nan-2309-handle-non-standard-linear-rate-limit
@khaliqgant
Copy link
Member Author

If we indeed have headers I would suggest something else

"x-complexity": "520",
      "x-ratelimit-complexity-limit": "1000000",
      "x-ratelimit-requests-remaining": "0",
      "x-ratelimit-requests-reset": "1733302464386",
      "x-ratelimit-complexity-reset": "1733302464386",

We should change the if in retry to search for headers whatever the status code

Thanks, agreed + less code. Updated with 47becfa

packages/shared/lib/services/proxy.service.ts Show resolved Hide resolved
@@ -259,7 +259,8 @@ class ProxyService {
(error.response?.status === 403 && error.response.headers['x-ratelimit-remaining'] && error.response.headers['x-ratelimit-remaining'] === '0') ||
error.response?.status === 429 ||
['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED'].includes(error.code as string) ||
config.retryOn?.includes(Number(error.response?.status))
config.retryOn?.includes(Number(error.response?.status)) ||
this.isRetryInHeader(config.provider.proxy?.retry, error)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just one last question (sorry my suggestion also had some shortcomings):
Are the headers always set or just when we reach rate limit? Here if its always set then all requests will be retried even on regular 400

I guess one missing check that does not exist yet in checking a "remaining headers"

Copy link
Collaborator

Choose a reason for hiding this comment

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

is isRetryInHeader supporting the special case for github 3 lines above?

Copy link
Member Author

Choose a reason for hiding this comment

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

Are the headers always set or just when we reach rate limit? Here if its always set then all requests will be retried even on regular 400

Unsure tbh which is why in my initial implementation kept this isolated to Linear to keep the impact low since I don't know how most APIs behave.

I guess one missing check that does not exist yet in checking a "remaining headers"

I don't know what you mean here? Can you clarify?

Copy link
Member Author

Choose a reason for hiding this comment

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

is isRetryInHeader supporting the special case for github 3 lines above?

No, but I guess we could encapsulate both into updated logic. Few proposals:

  1. just keep them both hard coded
# github
(error.response?.status === 403 && error.response.headers['x-ratelimit-remaining'] && error.response.headers['x-ratelimit-remaining'] === '0') ||

# linear 
(error.response?.status === 400 && error.response.headers['x-ratelimit-requests-remaining'] && error.response.headers['x-ratelimit-requests-remaining'] === '0') ||
  1. Codify it in the yaml
# github
proxy:
        retry:
            at: 'x-ratelimit-reset'
            remaining: x-ratelimit-remaining
            error_code: 403

# linear
proxy:
           retry:
              at: 'X-RateLimit-Requests-Reset'
              remaining: x-ratelimit-requests-remaining
              error_code: 400

any thoughts @NangoHQ/engineers?

auth_mode: 'OAUTH2',
proxy: {
retry: {
at: 'x-ratelimit-requests-resets'
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would make it very different from the real header name. It took me a few seconds to spot the extra s

@@ -505,6 +505,97 @@ describe('Proxy service Construct URL Tests', () => {
expect(diff).toBeGreaterThan(1000);
expect(diff).toBeLessThan(2000);
});

it('Should retry based on the header even if the error code is not a 429', async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we wrap those new tests in their own describe so you can instanciate the mockAxiosError only once?

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