mirror of
https://github.com/misenhower/splatoon2.ink.git
synced 2026-09-28 12:16:54 -05:00
Bound upstream and screenshot requests with network deadlines
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { fetchWithTimeout } from '../../common/fetch.js';
|
||||
|
||||
// Screenshots of the site's screenshot page, rendered by Cloudflare Browser Rendering's REST
|
||||
// API. It is plain fetch, so the same code runs under Node and in a Worker. The page is the
|
||||
// deployed one (SITE_URL), which reads the published data.
|
||||
@@ -8,7 +10,7 @@
|
||||
// CLOUDFLARE_BROWSER_RUN_API_TOKEN an API token with Browser Rendering permission
|
||||
|
||||
const viewport = {
|
||||
// Using a 16:9 ratio here by default to match Twitter's image card dimensions
|
||||
// Use a 16:9 ratio for the public social image.
|
||||
// 1216 was chosen as the width because of Bulma's "widescreen" breakpoint
|
||||
width: 1216,
|
||||
height: 684,
|
||||
@@ -56,7 +58,7 @@ export async function captureScreenshot({ hash, viewport: viewportOverrides, for
|
||||
let endpoint = new URL(`/client/v4/accounts/${accountId}/browser-rendering/screenshot`, 'https://api.cloudflare.com');
|
||||
endpoint.searchParams.set('cacheTTL', '0');
|
||||
|
||||
let response = await fetch(endpoint, {
|
||||
let response = await fetchWithTimeout(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiToken}`,
|
||||
@@ -65,10 +67,12 @@ export async function captureScreenshot({ hash, viewport: viewportOverrides, for
|
||||
body: JSON.stringify({
|
||||
url: url.toString(),
|
||||
viewport: thisViewport,
|
||||
gotoOptions: { waitUntil: 'networkidle0' }, // Wait until the network is idle
|
||||
gotoOptions: { waitUntil: 'networkidle0', timeout: 30_000 },
|
||||
actionTimeout: 30_000,
|
||||
setExtraHTTPHeaders: { 'Cache-Control': 'no-cache' },
|
||||
screenshotOptions: format === 'jpeg' ? { type: 'jpeg', quality: 90 } : { type: 'png' },
|
||||
}),
|
||||
});
|
||||
}, 90_000);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(`Browser Rendering screenshot failed (${response.status}): ${await errorMessage(response)}`);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fetchWithTimeout } from '../../../common/fetch.js';
|
||||
import { BskyAgent, RichText } from '@atproto/api';
|
||||
|
||||
export default class BlueskyClient
|
||||
@@ -22,14 +23,16 @@ export default class BlueskyClient
|
||||
|
||||
async login() {
|
||||
if (!this.#agent) {
|
||||
this.#agent = new BskyAgent({
|
||||
let agent = new BskyAgent({
|
||||
service: process.env.BLUESKY_SERVICE,
|
||||
fetch: fetchWithTimeout,
|
||||
});
|
||||
|
||||
await this.#agent.login({
|
||||
await agent.login({
|
||||
identifier: process.env.BLUESKY_IDENTIFIER,
|
||||
password: process.env.BLUESKY_PASSWORD,
|
||||
});
|
||||
this.#agent = agent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7
src/common/fetch.js
Normal file
7
src/common/fetch.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// A deadline covers both receiving headers and consuming the response body.
|
||||
export function fetchWithTimeout(input, init = {}, timeoutMs = 30_000) {
|
||||
let timeout = AbortSignal.timeout(timeoutMs);
|
||||
let existing = init.signal ?? (input instanceof Request ? input.signal : null);
|
||||
let signal = existing ? AbortSignal.any([existing, timeout]) : timeout;
|
||||
return fetch(input, { ...init, signal });
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { fetchWithTimeout } from './fetch.js';
|
||||
|
||||
// SplatNet2 API
|
||||
const splatnetBaseUrl = 'https://app.splatoon2.nintendo.net';
|
||||
|
||||
@@ -29,7 +31,7 @@ export default class SplatNet {
|
||||
/** Fetch a path (or absolute URL) on the SplatNet site with the session cookie. */
|
||||
async request(path, { headers = this.getHeaders() } = {}) {
|
||||
let url = new URL(path, splatnetBaseUrl);
|
||||
let response = await fetch(url, { headers });
|
||||
let response = await fetchWithTimeout(url, { headers });
|
||||
if (!response.ok)
|
||||
throw new Error(`SplatNet request failed with status ${response.status}: ${url.pathname}`);
|
||||
return response;
|
||||
|
||||
45
test/fetch.test.js
Normal file
45
test/fetch.test.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { test, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
import { fetchWithTimeout } from '../src/common/fetch.js';
|
||||
|
||||
test('aborts a stalled request at its deadline', async () => {
|
||||
mock.method(globalThis, 'fetch', (input, { signal }) => new Promise((resolve, reject) => {
|
||||
signal.addEventListener('abort', () => reject(signal.reason), { once: true });
|
||||
}));
|
||||
try {
|
||||
await Promise.all([
|
||||
assert.rejects(fetchWithTimeout('https://example.test', {}, 5), { name: 'TimeoutError' }),
|
||||
sleep(20),
|
||||
]);
|
||||
} finally {
|
||||
mock.restoreAll();
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps the deadline active while reading the response body', async () => {
|
||||
mock.method(globalThis, 'fetch', async (input, { signal }) => new Response(new ReadableStream({
|
||||
start(controller) {
|
||||
signal.addEventListener('abort', () => controller.error(signal.reason), { once: true });
|
||||
},
|
||||
})));
|
||||
try {
|
||||
const response = await fetchWithTimeout('https://example.test', {}, 5);
|
||||
await Promise.all([assert.rejects(response.text(), { name: 'TimeoutError' }), sleep(20)]);
|
||||
} finally {
|
||||
mock.restoreAll();
|
||||
}
|
||||
});
|
||||
|
||||
test('preserves caller cancellation', async () => {
|
||||
let signal;
|
||||
mock.method(globalThis, 'fetch', async (input, init) => { signal = init.signal; return new Response('ok'); });
|
||||
try {
|
||||
const controller = new AbortController;
|
||||
await fetchWithTimeout('https://example.test', { signal: controller.signal });
|
||||
controller.abort();
|
||||
assert.equal(signal.aborted, true);
|
||||
} finally {
|
||||
mock.restoreAll();
|
||||
}
|
||||
});
|
||||
@@ -30,8 +30,10 @@ test('asks Browser Rendering for the deployed screenshot page at the default vie
|
||||
assert.equal(headers.get('authorization'), 'Bearer token');
|
||||
assert.equal(body.url, 'https://example.test/screenshots.html#/schedules/3600');
|
||||
assert.deepEqual(body.viewport, { width: 1216, height: 684, deviceScaleFactor: 2 });
|
||||
assert.deepEqual(body.gotoOptions, { waitUntil: 'networkidle0' });
|
||||
assert.deepEqual(body.gotoOptions, { waitUntil: 'networkidle0', timeout: 30_000 });
|
||||
assert.deepEqual(body.screenshotOptions, { type: 'png' });
|
||||
assert.equal(body.actionTimeout, 30_000);
|
||||
assert.deepEqual(body.setExtraHTTPHeaders, { 'Cache-Control': 'no-cache' });
|
||||
assert.deepEqual(result, { image: PNG, type: 'image/png', width: 2432, height: 1368 });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user