fix: /run/{name} → 302 redirect, bypass CF same-zone fetch limitation
CF Workers cannot fetch() other workers on the same .workers.dev zone.
This caused all /run/{name} routes to return Cloudflare's HTML 404
instead of proxying to the sub-worker.
Fix: replace inline fetch() proxy with a redirect-based approach:
- Default (browser/curl): 302 redirect to sub-worker URL
- Accept: application/json: return JSON with {url, capability, cold_start}
LRU bookkeeping (page-in, access count) still happens in Sigil before
the redirect, so cold capabilities are warmed up transparently.
New backend method: resolveInvoke() — same LRU/page-in logic as invoke()
but returns route info instead of executing the subrequest.
Fixes: https://sigil.shazhou.workers.dev/run/* returning CF 404
Reported-by: 小墨 🖊️
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { generateWorkerCode } from '../src/codegen.js'
|
||||
import type { InputSchema } from '../src/codegen.js'
|
||||
|
||||
// Helper: eval the generated Worker code and call its fetch handler
|
||||
async function callWorker(
|
||||
code: string,
|
||||
options: {
|
||||
method?: string
|
||||
url?: string
|
||||
body?: unknown
|
||||
searchParams?: Record<string, string>
|
||||
} = {},
|
||||
): Promise<Response> {
|
||||
const { method = 'GET', url = 'https://example.com/', body, searchParams } = options
|
||||
|
||||
// Build URL with search params
|
||||
const reqUrl = new URL(url)
|
||||
if (searchParams) {
|
||||
for (const [k, v] of Object.entries(searchParams)) {
|
||||
reqUrl.searchParams.set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
const init: RequestInit = { method }
|
||||
if (body !== undefined && (method === 'POST' || method === 'PUT')) {
|
||||
init.body = JSON.stringify(body)
|
||||
init.headers = { 'Content-Type': 'application/json' }
|
||||
}
|
||||
|
||||
const request = new Request(reqUrl.toString(), init)
|
||||
|
||||
// Evaluate the worker code and get the default export
|
||||
const module = await import(/* @vite-ignore */ `data:text/javascript,${encodeURIComponent(code)}`)
|
||||
const worker = module.default
|
||||
return worker.fetch(request)
|
||||
}
|
||||
|
||||
describe('codegen: generateWorkerCode', () => {
|
||||
// Test 1: 基本代码生成
|
||||
it('schema + execute → 生成有效 Worker 代码', () => {
|
||||
const schema: InputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Name to greet' },
|
||||
},
|
||||
}
|
||||
const execute = `return "Hello, " + (input.name || "World") + "!"`
|
||||
const code = generateWorkerCode(schema, execute)
|
||||
|
||||
expect(code).toContain('export default')
|
||||
expect(code).toContain('async fetch(request)')
|
||||
expect(code).toContain('input.name')
|
||||
expect(typeof code).toBe('string')
|
||||
})
|
||||
|
||||
// Test 2: 类型转换 — number/boolean 从 query string 正确转换
|
||||
it('number 类型从 query string 正确转换', async () => {
|
||||
const schema: InputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
amount: { type: 'number', description: 'Amount' },
|
||||
},
|
||||
}
|
||||
const execute = `return JSON.stringify({ amount: input.amount, type: typeof input.amount })`
|
||||
const code = generateWorkerCode(schema, execute)
|
||||
|
||||
const resp = await callWorker(code, { searchParams: { amount: '42.5' } })
|
||||
expect(resp.status).toBe(200)
|
||||
const data = await resp.json() as { amount: number; type: string }
|
||||
expect(data.amount).toBe(42.5)
|
||||
expect(data.type).toBe('number')
|
||||
})
|
||||
|
||||
it('boolean 类型从 query string 正确转换', async () => {
|
||||
const schema: InputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
flag: { type: 'boolean', description: 'A flag' },
|
||||
},
|
||||
}
|
||||
const execute = `return JSON.stringify({ flag: input.flag, type: typeof input.flag })`
|
||||
const code = generateWorkerCode(schema, execute)
|
||||
|
||||
const respTrue = await callWorker(code, { searchParams: { flag: 'true' } })
|
||||
const dataTrue = await respTrue.json() as { flag: boolean; type: string }
|
||||
expect(dataTrue.flag).toBe(true)
|
||||
expect(dataTrue.type).toBe('boolean')
|
||||
|
||||
const respFalse = await callWorker(code, { searchParams: { flag: 'false' } })
|
||||
const dataFalse = await respFalse.json() as { flag: boolean; type: string }
|
||||
expect(dataFalse.flag).toBe(false)
|
||||
})
|
||||
|
||||
// Test 3: 默认值填充
|
||||
it('缺少参数时用默认值', async () => {
|
||||
const schema: InputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
amount: { type: 'number', description: 'Amount', default: 1 },
|
||||
currency: { type: 'string', description: 'Currency', default: 'USD' },
|
||||
},
|
||||
}
|
||||
const execute = `return JSON.stringify({ amount: input.amount, currency: input.currency })`
|
||||
const code = generateWorkerCode(schema, execute)
|
||||
|
||||
const resp = await callWorker(code)
|
||||
expect(resp.status).toBe(200)
|
||||
const data = await resp.json() as { amount: number; currency: string }
|
||||
expect(data.amount).toBe(1)
|
||||
expect(data.currency).toBe('USD')
|
||||
})
|
||||
|
||||
// Test 4: required 校验 — 缺少必填参数返回 400
|
||||
it('缺少 required 参数返回 400', async () => {
|
||||
const schema: InputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string' },
|
||||
to: { type: 'string' },
|
||||
},
|
||||
required: ['from', 'to'],
|
||||
}
|
||||
const execute = `return JSON.stringify({ from: input.from, to: input.to })`
|
||||
const code = generateWorkerCode(schema, execute)
|
||||
|
||||
// Missing both required params
|
||||
const resp1 = await callWorker(code)
|
||||
expect(resp1.status).toBe(400)
|
||||
const data1 = await resp1.json() as { error: string }
|
||||
expect(data1.error).toContain('Missing required parameter: from')
|
||||
|
||||
// Only `from` provided
|
||||
const resp2 = await callWorker(code, { searchParams: { from: 'USD' } })
|
||||
expect(resp2.status).toBe(400)
|
||||
const data2 = await resp2.json() as { error: string }
|
||||
expect(data2.error).toContain('Missing required parameter: to')
|
||||
|
||||
// Both provided — should succeed
|
||||
const resp3 = await callWorker(code, { searchParams: { from: 'USD', to: 'CNY' } })
|
||||
expect(resp3.status).toBe(200)
|
||||
})
|
||||
|
||||
// Test 5: 空 schema — 无参数的函数
|
||||
it('空 schema — 无参数的函数正常运行', async () => {
|
||||
const schema: InputSchema = { properties: {} }
|
||||
const execute = `return "hello world"`
|
||||
const code = generateWorkerCode(schema, execute)
|
||||
|
||||
const resp = await callWorker(code)
|
||||
expect(resp.status).toBe(200)
|
||||
const text = await resp.text()
|
||||
expect(text).toBe('hello world')
|
||||
})
|
||||
|
||||
// Test 6: POST body 解析 — JSON body 正确读取
|
||||
it('POST body 解析 — JSON body 正确读取', async () => {
|
||||
const schema: InputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
x: { type: 'number' },
|
||||
y: { type: 'number' },
|
||||
},
|
||||
}
|
||||
const execute = `return JSON.stringify({ sum: input.x + input.y })`
|
||||
const code = generateWorkerCode(schema, execute)
|
||||
|
||||
const resp = await callWorker(code, {
|
||||
method: 'POST',
|
||||
body: { x: 10, y: 20 },
|
||||
})
|
||||
expect(resp.status).toBe(200)
|
||||
const data = await resp.json() as { sum: number }
|
||||
expect(data.sum).toBe(30)
|
||||
})
|
||||
|
||||
// Test 7: 错误处理 — execute 抛错返回 500
|
||||
it('execute 抛错返回 500', async () => {
|
||||
const schema: InputSchema = { properties: {} }
|
||||
const execute = `throw new Error("intentional error")`
|
||||
const code = generateWorkerCode(schema, execute)
|
||||
|
||||
const resp = await callWorker(code)
|
||||
expect(resp.status).toBe(500)
|
||||
const data = await resp.json() as { error: string }
|
||||
expect(data.error).toContain('intentional error')
|
||||
})
|
||||
|
||||
// Test 8: query params override POST body
|
||||
it('query params 覆盖 POST body 同名参数', async () => {
|
||||
const schema: InputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
value: { type: 'string' },
|
||||
},
|
||||
}
|
||||
const execute = `return input.value`
|
||||
const code = generateWorkerCode(schema, execute)
|
||||
|
||||
const resp = await callWorker(code, {
|
||||
method: 'POST',
|
||||
url: 'https://example.com/?value=from-query',
|
||||
body: { value: 'from-body' },
|
||||
})
|
||||
expect(resp.status).toBe(200)
|
||||
const text = await resp.text()
|
||||
// query params should override body
|
||||
expect(text).toBe('from-query')
|
||||
})
|
||||
|
||||
// Test 9: non-string output auto-stringified
|
||||
it('非 string 返回值自动 JSON 序列化', async () => {
|
||||
const schema: InputSchema = { properties: {} }
|
||||
const execute = `return { hello: "world", num: 42 }`
|
||||
const code = generateWorkerCode(schema, execute)
|
||||
|
||||
const resp = await callWorker(code)
|
||||
expect(resp.status).toBe(200)
|
||||
const data = await resp.json() as { hello: string; num: number }
|
||||
expect(data.hello).toBe('world')
|
||||
expect(data.num).toBe(42)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user