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

[Feature] Add signal option to allow aborting requests #1972

Merged
merged 4 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/green-snails-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@shopify/shopify-app-remix': minor
'@shopify/shopify-api': minor
---

# Adds signal as request option

This adds the `signal` option to the `request` method of the GraphQL client, for the shopify-api and shopify-app-remix packages to pass in an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) to abort requests, and set a timeout.

If a request is aborted, an `HttpRequestError` will be thrown.

This will allow you to set your own custom timeout, and abort requests.

```ts
// Abort the request after 3 seconds
await admin.graphql('{ shop { name } }', {
signal: AbortSignal.timeout(3000),
});
```

```ts
// Abort the request after 3 seconds, and retry the request up to 2 times
await admin.graphql('{ shop { name } }', {
signal: AbortSignal.timeout(3000),
tries: 2,
});
```
39 changes: 37 additions & 2 deletions packages/apps/shopify-api/docs/reference/clients/Graphql.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ const response = await client.request(
console.log(response.data, response.extensions, response.headers);
```

> **Note**: If using TypeScript, you can pass in a type argument for the response body:
#### Using a type argument

If using TypeScript, you can pass in a type argument for the response body:

```ts
// If using TypeScript, you can type the response body
Expand All @@ -110,7 +112,9 @@ const response = await client.request<MyResponseBodyType>(/* ... */);
console.log(response.body.data);
```

> **Note**: If there are any errors in the response, `request` will throw a `GraphqlQueryError` which includes details from the API response:
#### Handling errors

If there are any errors in the response, `request` will throw a `GraphqlQueryError` which includes details from the API response:

```ts
import {GraphqlQueryError} from '@shopify/shopify-api';
Expand All @@ -130,6 +134,31 @@ try {
}
```

#### Setting a timeout
You can set a timeout for the request by passing in a signal with an AbortController. If the request takes longer than the timeout, it will be aborted and an AbortError will be thrown.

```ts
const response = await client.request(
`query GetProducts($first: Int!) {
products (first: $first) {
edges {
node {
id
title
descriptionHtml
}
}
}
}`,
{
variables: {
first: 10,
},
signal: AbortSignal.timeout(3000), // 3 seconds
},
);
```

### Parameters

#### operation
Expand All @@ -156,6 +185,12 @@ Add custom headers to the request.

The maximum number of times to retry the request.

#### options.signal

`AbortSignal`

An optional AbortSignal to cancel the request.

### Return

`Promise<GraphQLClientResponse>`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,16 @@ describe('GraphQL client', () => {
),
);
});

it('respects the abort signal', async () => {
const shopify = shopifyApi(testConfig());
const client = new shopify.clients.Graphql({session});
const controller = new AbortController();

controller.abort();

await expect(
client.request(QUERY, {signal: controller.signal}),
).rejects.toThrow(HttpRequestError);
});
});
4 changes: 4 additions & 0 deletions packages/apps/shopify-api/lib/clients/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ export interface GraphqlQueryOptions<
* The maximum number of times to retry the request if it fails with a throttling or server error.
*/
retries?: number;
/**
* An optional AbortSignal to cancel the request.
*/
signal?: AbortSignal;
}

export {GraphqlClient} from './admin/graphql/client';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ describe('admin.authenticate context', () => {
).rejects.toThrowError(HttpMaxRetriesError);
});

it('respects the abort signal', async () => {
// GIVEN
const {admin} = await setUpEmbeddedFlow();
const controller = new AbortController();
await mockGraphqlRequest()({status: 200});

// Abort the request immediately
controller.abort();

// WHEN/THEN
await expect(
admin.graphql('{ shop { name } }', {signal: controller.signal}),
).rejects.toThrowError(Error);
});

it('re-throws errors other than HttpResponseErrors on REST requests', async () => {
// GIVEN
const {admin} = await setUpEmbeddedFlow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function graphqlClientFactory({
variables: options?.variables,
retries: options?.tries ? options.tries - 1 : 0,
headers: options?.headers,
signal: options?.signal,
});

return new Response(JSON.stringify(apiResponse));
Expand Down
5 changes: 5 additions & 0 deletions packages/apps/shopify-app-remix/src/server/clients/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export interface GraphQLQueryOptions<
* The total number of times to try the request if it fails.
*/
tries?: number;

/**
* An optional AbortSignal to cancel the request.
*/
signal?: AbortSignal;
}

export type GraphQLResponse<
Expand Down
Loading