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
7 changes: 6 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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, () =>
Expand Down
14 changes: 4 additions & 10 deletions src/utils/github-content-cache.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ export class InvalidCacheKeyError extends Error {
const pendingRefreshes = new Map<string, Promise<void>>()
const memoryGitHubContent = new Map<string, GithubContentRecord>()
const memoryDocsArtifacts = new Map<string, DocsArtifactRecord>()
let blobBackend: CacheBackend | undefined

function assertValidRepo(repo: string) {
if (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1525,5 +1520,4 @@ export function resetGitHubContentCacheForTest() {
pendingRefreshes.clear()
memoryGitHubContent.clear()
memoryDocsArtifacts.clear()
blobBackend = undefined
}
6 changes: 6 additions & 0 deletions src/utils/http.ts
Original file line number Diff line number Diff line change
@@ -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)
}
41 changes: 41 additions & 0 deletions tests/github-content-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,46 @@ async function testBlobStorageInfersGithubContentMetadataFromKey() {
assert.equal(originCalls, 0)
}

async function testBlobStorageUsesCurrentRequestBinding() {
resetGitHubContentCacheForTest()

const mockR2 = createMockR2Bucket()
const reads: Array<number> = []
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()

Expand Down Expand Up @@ -388,6 +428,7 @@ async function testStaleArtifactRefreshesInWaitUntil() {
await testMissStoresContent()
await testFreshHitSkipsOrigin()
await testWorkerEnvUsesBlobStorage()
await testBlobStorageUsesCurrentRequestBinding()
await testBlobStorageInfersGithubContentMetadataFromKey()
await testForcedStaleRefreshes()
await testNegativeHitSkipsOrigin()
Expand Down
35 changes: 35 additions & 0 deletions tests/https-redirect.test.ts
Original file line number Diff line number Diff line change
@@ -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)
}
})
Loading