From 8e8cf8a8a6b1554bc7bdfdb356ba454da8957be9 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 9 Oct 2026 23:41:34 -0600 Subject: [PATCH] Fix insecure site requests and docs cache binding lifetime --- src/server.ts | 7 +++- src/utils/github-content-cache.server.ts | 14 +++----- src/utils/http.ts | 6 ++++ tests/github-content-cache.test.ts | 41 ++++++++++++++++++++++++ tests/https-redirect.test.ts | 35 ++++++++++++++++++++ 5 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 tests/https-redirect.test.ts diff --git a/src/server.ts b/src/server.ts index 91f219ed6..a802998f6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -21,7 +21,10 @@ import { logRequestStart, runWithRequestDiagnostics, } from '~/utils/prod-diagnostics.server' -import { docsContentNegotiationVaryHeader } from '~/utils/http' +import { + docsContentNegotiationVaryHeader, + redirectInsecureSiteRequest, +} from '~/utils/http' import { isFrameEmbeddingAllowed } from '~/utils/frame-embedding' import { redirectChatOrigin } from './chat/server/origin-redirect' @@ -277,6 +280,8 @@ const server = createServerEntry( export default { fetch(request: Request, env: unknown, context: unknown) { + const secureSite = redirectInsecureSiteRequest(request) + if (secureSite) return secureSite const canonicalChat = redirectChatOrigin(request) if (canonicalChat) return canonicalChat return runWithHostRuntimeEnv(env, () => diff --git a/src/utils/github-content-cache.server.ts b/src/utils/github-content-cache.server.ts index 03129f5ba..7fc248527 100644 --- a/src/utils/github-content-cache.server.ts +++ b/src/utils/github-content-cache.server.ts @@ -145,7 +145,6 @@ export class InvalidCacheKeyError extends Error { const pendingRefreshes = new Map>() const memoryGitHubContent = new Map() const memoryDocsArtifacts = new Map() -let blobBackend: CacheBackend | undefined function assertValidRepo(repo: string) { if ( @@ -1207,14 +1206,10 @@ async function getCacheBackend() { const storage = await getBlobStorage(GITHUB_CONTENT_BLOB_STORAGE) if (storage) { - if (!blobBackend) { - blobBackend = createBlobBackend( - storage, - getBlobStorageCache(GITHUB_CONTENT_BLOB_STORAGE), - ) - } - - return blobBackend + return createBlobBackend( + storage, + getBlobStorageCache(GITHUB_CONTENT_BLOB_STORAGE), + ) } return memoryBackend @@ -1525,5 +1520,4 @@ export function resetGitHubContentCacheForTest() { pendingRefreshes.clear() memoryGitHubContent.clear() memoryDocsArtifacts.clear() - blobBackend = undefined } diff --git a/src/utils/http.ts b/src/utils/http.ts index 1ce4d2f12..6bae76ff6 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -1,2 +1,8 @@ export const docsContentNegotiationVaryHeader = 'Accept, Accept-Encoding, Sec-Fetch-Dest, Sec-Fetch-Mode' + +export function redirectInsecureSiteRequest(request: Request) { + if (!request.url.startsWith('http://tanstack.com/')) return + + return Response.redirect(request.url.replace(/^http:/, 'https:'), 308) +} diff --git a/tests/github-content-cache.test.ts b/tests/github-content-cache.test.ts index 1d24a2ab6..f2a85b455 100644 --- a/tests/github-content-cache.test.ts +++ b/tests/github-content-cache.test.ts @@ -148,6 +148,46 @@ async function testBlobStorageInfersGithubContentMetadataFromKey() { assert.equal(originCalls, 0) } +async function testBlobStorageUsesCurrentRequestBinding() { + resetGitHubContentCacheForTest() + + const mockR2 = createMockR2Bucket() + const reads: Array = [] + const requestBucket = (requestId: number) => ({ + ...mockR2.bucket, + async get(key: string) { + reads.push(requestId) + return mockR2.bucket.get(key) + }, + }) + let originCalls = 0 + const opts = { + repo, + gitRef, + path: 'docs/request-binding.md', + origin: async () => { + originCalls += 1 + return 'shared cached content' + }, + } + + for (const requestId of [1, 2]) { + const readOffset = reads.length + const result = await runWithHostRuntimeEnv( + { GITHUB_CONTENT_CACHE: requestBucket(requestId) }, + () => getCachedGitHubTextFile(opts), + ) + assert.equal(result, 'shared cached content') + assert.ok(reads.length > readOffset) + assert.ok( + reads.slice(readOffset).every((id) => id === requestId), + 'blob reads must use the current request binding', + ) + } + + assert.equal(originCalls, 1) +} + async function testForcedStaleRefreshes() { resetGitHubContentCacheForTest() @@ -388,6 +428,7 @@ async function testStaleArtifactRefreshesInWaitUntil() { await testMissStoresContent() await testFreshHitSkipsOrigin() await testWorkerEnvUsesBlobStorage() +await testBlobStorageUsesCurrentRequestBinding() await testBlobStorageInfersGithubContentMetadataFromKey() await testForcedStaleRefreshes() await testNegativeHitSkipsOrigin() diff --git a/tests/https-redirect.test.ts b/tests/https-redirect.test.ts new file mode 100644 index 000000000..6aa15f284 --- /dev/null +++ b/tests/https-redirect.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { redirectInsecureSiteRequest } from '../src/utils/http' + +test('HTTP site requests reach HTTPS before browser crypto code runs', () => { + for (const method of ['GET', 'HEAD', 'POST']) { + const response = redirectInsecureSiteRequest( + new Request( + 'http://tanstack.com/query/latest/docs/framework/react/overview?search=a%2Fb&returnTo=%2Fdocs', + { method }, + ), + ) + assert.ok(response) + assert.equal(response.status, 308) + assert.equal( + response.headers.get('Location'), + 'https://tanstack.com/query/latest/docs/framework/react/overview?search=a%2Fb&returnTo=%2Fdocs', + ) + assert.equal(response.body, null) + } +}) + +test('HTTPS and other hosts retain their own routing', () => { + for (const url of [ + 'https://tanstack.com/', + 'http://localhost:3000/query/latest/docs/framework/react/overview', + 'http://127.0.0.1:3000/', + 'http://preview.example.com/', + 'http://chat.tanstack.com/', + 'http://tanstack.com.attacker.test/', + 'http://tanstack.com:8080/', + ]) { + assert.equal(redirectInsecureSiteRequest(new Request(url)), undefined, url) + } +})