Skip to content
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
62 changes: 29 additions & 33 deletions docs/cloud/headless-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function getRetryDelay(response, attempt) {
return backoff;
}

export async function fetchWithRetry(input, init = {}) {
export async function fetchWithRetry(request) {
const deadline = Date.now() + TOTAL_TIMEOUT;

for (let attempt = 0; ; attempt++) {
Expand All @@ -73,11 +73,11 @@ export async function fetchWithRetry(input, init = {}) {
throw new Error('Craft request timed out');
}

const timeoutSignal = AbortSignal.timeout(remaining);
const signal = init.signal
? AbortSignal.any([init.signal, timeoutSignal])
: timeoutSignal;
const response = await fetch(input, { ...init, signal });
const signal = AbortSignal.any([
request.signal,
AbortSignal.timeout(remaining),
]);
const response = await fetch(request.clone(), { signal });

if (response.ok) {
return response;
Expand Down Expand Up @@ -105,8 +105,8 @@ export async function fetchWithRetry(input, init = {}) {

Create a `request-signatures.js` module using `getSignatureHeaders()` from the
general [Node.js signing example](request-signing.md#from-node-js). The
framework examples below import that helper so signing does not interfere with
framework-specific request options.
framework examples below construct and sign a native `Request` before sending
it.

## Next.js Example

Expand All @@ -131,14 +131,14 @@ const headers = {

const getBlogEntries = unstable_cache(
async () => {
const signatureHeaders = getSignatureHeaders({ method, url, headers });
const result = await ky.post(url, {
body,
const request = new Request(url, { method, body, headers });

for (const [name, value] of Object.entries(getSignatureHeaders(request))) {
request.headers.set(name, value);
}

const result = await ky(request, {
cache: 'no-store',
headers: {
...headers,
...signatureHeaders,
},
retry: {
limit: Number.POSITIVE_INFINITY,
methods: ['post'],
Expand Down Expand Up @@ -197,15 +197,13 @@ const headers = {
};

export default defineEventHandler(async () => {
const signatureHeaders = getSignatureHeaders({ method, url, headers });
const response = await fetchWithRetry(url, {
method,
body,
headers: {
...headers,
...signatureHeaders,
},
});
const request = new Request(url, { method, body, headers });

for (const [name, value] of Object.entries(getSignatureHeaders(request))) {
request.headers.set(name, value);
}

const response = await fetchWithRetry(request);
const result = await response.json();

if (result.errors?.length) {
Expand Down Expand Up @@ -252,15 +250,13 @@ const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CRAFT_GRAPHQL_TOKEN}`,
};
const signatureHeaders = getSignatureHeaders({ method, url, headers });
const response = await fetchWithRetry(url, {
method,
body,
headers: {
...headers,
...signatureHeaders,
},
});
const request = new Request(url, { method, body, headers });

for (const [name, value] of Object.entries(getSignatureHeaders(request))) {
request.headers.set(name, value);
}

const response = await fetchWithRetry(request);
const result = await response.json();

if (result.errors?.length) {
Expand Down
68 changes: 32 additions & 36 deletions docs/cloud/request-signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,70 +49,66 @@ export function getSignatureHeaders(
) {
const created = new Date();

return signatureHeadersSync(
request,
{
key: 'sig',
signer: {
keyid: 'hmac',
alg: 'hmac-sha256',
signSync(data) {
return crypto
.createHmac('sha256', CRAFT_CLOUD_SIGNING_KEY)
.update(data)
.digest();
},
return signatureHeadersSync(request, {
key: 'sig',
signer: {
keyid: 'hmac',
alg: 'hmac-sha256',
signSync(data) {
return crypto
.createHmac('sha256', CRAFT_CLOUD_SIGNING_KEY)
.update(data)
.digest();
},
components,
created,
tag: 'craft-cloud',
},
components,
created,
tag: 'craft-cloud',

// Optional expiry. The maximum is five minutes.
// expires: new Date(created.getTime() + 60 * 1000),
}
);
// Optional expiry. The maximum is five minutes.
// expires: new Date(created.getTime() + 60 * 1000),
});
}
```

Pass additional [covered components](https://www.rfc-editor.org/rfc/rfc9421.html#name-http-message-components), such as `content-type`, in the second argument when those values must also be signed.

Import the helper when sending a signed request:
Construct a native `Request`, sign it, then send the same object:

```js
import { getSignatureHeaders } from './request-signatures.js';

const request = {
method: 'POST',
url: 'https://my-env.some-domain.com/api',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer my-secret-gql-schema-token',
},
};
const url = new URL('https://my-env.some-domain.com/api');

const body = JSON.stringify({
query: `{ entries(section: "blog") { title url } }`,
});

const signatureHeaders = getSignatureHeaders(request);

const response = await fetch(request.url, {
method: request.method,
const request = new Request(url, {
method: 'POST',
headers: {
...request.headers,
...signatureHeaders,
'Content-Type': 'application/json',
'Authorization': 'Bearer my-secret-gql-schema-token',
},
body,
});

for (const [name, value] of Object.entries(getSignatureHeaders(request))) {
request.headers.set(name, value);
}

const response = await fetch(request);

if (!response.ok) {
throw new Error(`Craft request failed: ${response.status}`);
}
```

::: tip
Requests signed using the `@target-uri` [component](https://www.rfc-editor.org/rfc/rfc9421.html#name-derived-components) are only valid when sent to a URL that matches _exactly_, including the scheme, hostname, path, and query string.
The example above satisfies this by using the same `request.url` value for signing and the `fetch()` call.
Constructing, signing, and sending the same `Request` object ensures that the signed URL matches the URL sent by `fetch()`.

When adding query parameters, use `url.searchParams.set(name, value)` rather than appending raw values. This encodes parameter names and values before the request is signed.
:::

### From Grafana Cloud k6
Expand Down
Loading