@ codumation-cc/ works- http (中文(简体) )
一个Cloudflare工人API的HTTP管道:抛出错误作为答案,一个API的路线,JSON的响应,JSON和身体读取
有限制的,客户的地址和网络,同源的cookie通话检查,对工人的费率限制
RaseLimit绑定,JSON的答案保存在工人的缓存, cookie, SHA-256, 以及一个单页的应用程序的页面
访问者写了地址 它运行在工人 和任何运行时间 Request,流和WebCrypto(20+节点)
一个依赖是 @codefusion-cc/workers-crypto。 。 。 .
npm install @codefusion-cc/workers-http
一个地方回答错误
手摇 HttpError 任何不是幸福的答案, 也一样 readJson, (中文(简体) ), readBody, (中文(简体) ),
requireSameOrigin, (中文(简体) ), rateLimit页:1 Router是因为 route 财务报告和审定财务报表 serveSinglePageApp工人将任何被扔到一个地方的反应:
import { clientNetwork, errorResponse, HttpError, json, rateLimit, readJson, requireSameOrigin } from '@codefusion-cc/workers-http'
export default {
async fetch(request, env) {
try {
return await route(request, env)
} catch (error) {
return errorResponse(error, { request }) // HttpError: its status and body; anything else: a 500 that reveals nothing, logged
}
},
} satisfies ExportedHandler<Env>
async function signIn(request: Request, env: Env) {
requireSameOrigin(request, [env.DEV_ORIGIN]) // 403 for a page on another site
await rateLimit(env.AUTH_LIMITER, clientNetwork(request)) // 429 with retry-after
const { email } = await readJson<{ email?: unknown }>(request) // 415, 413, 400
if (typeof email !== 'string') throw new HttpError(422, 'Enter your e-mail', { fieldErrors: { email: 'Required' } })
return json({ ok: true })
}
一个 HttpError 答题 { error, code?, fieldErrors?, ...data } 及其地位和 headers它的地位必须是:
400至599(其他任何东西都投出RangeError). errorResponse 还回答另一个安装的复制件所作的一个
包( 哪个) instanceof 因此,在另一个软件包中抛出的错误仍然得到其状态.
鉴于 request, (中文(简体) ), errorResponse 也取消了一个没有读到的身体, 所以一个上传在读之前被拒绝了
(a 401 on a PUT) 即刻停止,而不会跑到它的尽头.
json(data, init) 发送 application/json; charset=utf-8 财务报告和审定财务报表 cache-control: no-store 除非 init 套装
他们. init.headers 可能是一个 Headers 数个 set-cookie。 。 。 . jsonError(status, message) 是同一个身体
作为被丢弃的HtpError, 为返回而不是扔.
应用语言
包裹信息为英语. errorResponse 用代码交换应用程序的措辞,留下任何
信件, 应用程序单独写( e)readBody是因为 message,它自己的 Http 错误 :
errorResponse(error, {
messages: {
unsupported_media_type: 'Wyślij dane jako JSON.',
body_too_large: 'Za duże zapytanie.',
length_required: 'Wyślij zapytanie z nagłówkiem Content-Length.',
unreadable_body: 'Nie udało się odczytać zapytania.',
invalid_json: 'Nieprawidłowe dane.',
cross_origin: 'Odrzucono zapytanie z innej strony.',
rate_limited: 'Zbyt wiele prób. Odczekaj minutę i spróbuj ponownie.',
not_found: 'Nie znaleziono.',
method_not_allowed: 'Ten adres nie przyjmuje takiego zapytania.',
server_error: 'Wystąpił błąd serwera. Spróbuj ponownie.',
},
onError: error => console.error('unhandled', error), // console.error by default; a throwing logger cannot break the answer
})
路线
Router 保留 API 的路径, 方法和路径 任何应用程序称为处理器
用于:
import { errorResponse, json, Router } from '@codefusion-cc/workers-http'
type Handler = (request: Request, env: Env, params: Record<string, string>) => Response | Promise<Response>
const api = new Router<Handler>()
.on('GET', '/api/orders/new', () => json({ draft: true })) // before the parameter that would take "new"
.on('GET', '/api/orders/:id', (_request, _env, { id }) => json({ id }))
.on('DELETE', '/api/orders/:id', removeOrder)
export default {
async fetch(request, env) {
try {
const { handler, params } = api.route(request.method, new URL(request.url).pathname) // 404, 405
return await handler(request, env, params)
} catch (error) {
return errorResponse(error)
}
},
} satisfies ExportedHandler<Env>
- A 类
:name段是一个参数: 它需要一个非空段, 它的值会到达 已解码的% (%)Bosch%20%2B实值Bosch +; 其编码被破损者将保持发送状态,解码值可以 拿着/) (中文(简体) ). 所有其它部分都碰巧碰面 信箱和后面的斜线 以及ASCII以外的文本 写得很清楚:/api/zamówienia满足浏览器的/api/zam%C3%B3wienia。 。 。 . - 在对固定文本进行比较之前,不读取请求中的脱逃:
/api/%61dmin/users没有/api/admin/users(该词 是一个404,或者一个参数的值。 因此,一个检查应用程序 在路径之前的路径,例如pathname.startsWith('/api/admin/'),而且路由器总是同意一个请求的路径。 参数 值被解码,所以/api/:area/users获得admin从%61dmin: 处理器从不为 路径. - 第一条路线增加了符合答案的路线. 一个方法被满足了,但是
HEAD: 一个路径的GET路由应答 当路径没有时HEAD它自己的路线,运行时间发送 答案没有它的躯体. route(method, pathname)掷出404枚not_found对于一条没有路径,和 405method_not_allowed与allow(路径确实回答的方法) 对于一个只有其他方法的路径.match返回为无效 对于两者, 对于一个应用 答案自己.on为编程错误扔出 TypeError : 一个不是大写字母的方法, 一个不是一个路径 (无领导)/页:1?页:1#页:1..页:1:部分不是:name,在一个路径中两次参数,和一个 第二条路由 对于一个方法 和路径有一个, 它永远无法回答.
阅读机构
| 打电话 | 拒绝 |
|---|---|
readJson(request, options) | 第415条 unsupported_media_type: 内容类型不 application/json 或者说 application/*+json. 413 (中文(简体) ) body_too_large. 400 (中文(简体) ) unreadable_body. 400 (中文(简体) ) invalid_json:没有身体,没有UTF-8,没有JSON,或者没有物体. |
readBody(request, options) | 413号 body_too_large、400个 unreadable_body (上传中断). 解析字节;无正文为空字节. 也需要一个答复,例如从另一个服务处得到的. |
streamBody(request, { maxBytes }, store) | 411号 length_required:无Content-Length,或非整数. 413号 body_too_large. 400 (中文(简体) ) unreadable_body: 或多或少比所申报的字节. 电话 store(body, size) 身体是一条溪流 size 字节,用于一个需要一串已知长度的流的商店,解决它所解决的问题. |
maxBytes(默认)MAX_BODY_BYTES, 256 KiB) 计数字节,而不是字符. 一个更大的内容 - Length 是 在读完任何内容之前被拒绝。 身体变长(被挤出,或被骗取头)被取消为 一旦超过限制,所以无尽的上传成本最多只能达到限制. 它拒绝的每个身体都被取消 在每一个出路readBody让我们去身体的溪流, 所以它从来没有被锁.message413个字'The photo is larger than 3 MB.'。 。 。 .- Content- Type 被检查, 因为跨站点表单可以张贴
text/plain没有飞行前,但没有application/json。 。 。 .anyContentType: true读取navigator.sendBeacon体 (单位:千人)text/plain;仅限打电话 没有饼干认证. - 只有对象通过,而不是数组或赤值,所以
const { name } = await readJson(request)无法 丢入类型错误. __proto__密钥被丢入每个深处, 所以Object.assign(row, body)无法改变原型.T是对编译器的保证,而不是检查:在使用字段之前先验证字段.
streamBody 将一个上传到商店而不保存在内存, R2 为一个:
const limit = { maxBytes: 100e6, message: 'An artifact is at most 100 MB.' }
await streamBody(request, limit, body => env.BUCKET.put(key, body)) // 411, 413, 400 when the upload broke
- 两次拒绝都会在字节读取前出现,并取消身体. 在工人中,溪流是一个
FixedLengthStream这样 R2知道它的长度. - 断断续续的躯体, 或带来多少字节 比它的内容 - Length,错误流,所以商店
而不是保存一个剪接副本,而且
streamBody丢出400unreadable_body:发件人的错. 任何其他 商店的故障(它停止阅读,R2已倒下)是按原样扔出,一500.
谁打电话
clientIp(request, fallback = 'unknown')数字 :cf-connecting-ip云花板,没有人能伪造 通过它.x-forwarded-for财务报告和审定财务报表x-real-ip永远不读.clientNetwork(request, fallback):按地址限制费率的密钥. IPv4地址保持完整. IPv6 地址 因为一个家庭或电话 持有整个64, 并且可以为每次尝试取一个新的地址. 地址的每个拼写都提供同样的钥匙::ffff:a.b.c.d给IPv4一个.requireSameOrigin(request, allowedOrigins):用于呼叫饼干认证。 它允许一个Origin这是 (b) 请求本身或所列(http://localhost:5173或无Origin与Sec-Fetch-Site: same-origin. 其他一切获得403cross_origin,包括:Origin: null没有标题 一点也不 它覆盖了WebSocket的升级,它们携带饼干跨站点. 所列条目不是来源 扔出类型错误.
费率限额
// wrangler.jsonc
"ratelimits": [{ "name": "AUTH_LIMITER", "namespace_id": "1001", "simple": { "limit": 10, "period": 60 } }]
rateLimit(env.AUTH_LIMITER, key, { message, retryAfter }) 数到1个请求和429
rate_limited 与 retry-after (默认60,约束期限)一旦达到限制. 一个束缚
错误无法拒绝, 因此一个已断开的限制器拒绝请求, 而不是让每个人都通过 .
单页应用程序的页面
"not_found_handling": "single-page-application" 回答页面路径, 资产拼写为 307 到
他们自己的拼写。 他们解码每个段 并再次编码它与 encodeURIComponent数字 :
| 页面写 | 资产改用于 | 页面读取 |
|---|---|---|
/Mac/search/house+of+the+dragon | /Mac/search/house%2Bof%2Bthe%2Bdragon | 外加, 而不是空格 |
/rooms/AC%2FDC | /rooms/AC/DC | 两个部分 |
/a//b | /a/b | 减少一个部分 |
/s/a:b, (中文(简体) ), /s/%c5%82, (中文(简体) ), /s/100% | /s/a%3Ab, (中文(简体) ), /s/%C5%82, (中文(简体) ), /s/100%25 | 同样的文字,经过不必要的往返 |
html_handling: "none" 不变。 因此,一个将文本保留在路径中的应用程序(搜索,名称)读取
重装后的其他东西。 没有这种处理方式("none",默认值)资产回答路径为无
带有平地 404 的文件,从不重定向,路由器将其交给工人. serveSinglePageApp 接电话
与应用程序的页面,地址为:
// wrangler.jsonc
"assets": {
"directory": "./dist",
"binding": "ASSETS"
// No not_found_handling: the Worker answers pages (serveSinglePageApp). Files never invoke the Worker.
}
async function route(request: Request, env: Env) {
const path = new URL(request.url).pathname
if (path.startsWith('/api/')) return (await handleApi(request, env)) ?? jsonError(404, 'Not found')
return serveSinglePageApp(request, env.ASSETS) // last, for every path the API does not own
}
- 它首先要求资产获取路径: 他们的答案是文件, 他们的307 要求的文件由
另一种拼写方式 (
/sw%2Ejs改为/sw.js, (中文(简体) ),/index.html改为/) (中文(简体) ). 只有404个成为了应用的一页,所以有效 相同的时候run_worker_first向工人发送文件( E)true,或["/*", "!/assets/*"]为工人 必须看到每个页面载荷),每个文件的工人引用. - 与
"single-page-application"资产会自己解决所有问题 把它拿出来. - 只有GET和HEAD是页面。 任何其他方法投出405
method_not_allowed与allow: GET, HEAD,则 不要求资产. - 页面是用请求的方法和标题询问的,所以重新装入
If-None-Match仍然得到304. _headers规则适用于网页,如同适用于网页/数字 :/*规则确实如此,对于一个页面本身路径的规则没有. 保留页面 信头(CSP,x-frame-options继续/*,或者把它们放在工人.{ shell: '/admin/' }用另一页的答案, 对于一个页面未在/外壳不是 此站点的路径以 TypeError 拒绝 .
保存在缓存中的答案
cachedJson 用JSON来回答一个价格昂贵的或头部,对每个人都一样(目录,产品,
(a) 价格清单)。 它将答案保存在工人的缓存中,所以大多数请求从不运行 build数字 :
import { cachedJson } from '@codefusion-cc/workers-http'
async function catalog(request: Request, env: Env, ctx: ExecutionContext) {
const key = new URL('/api/catalog', request.url) // one spelling of the question, however a visitor writes it
return cachedJson(request, { cache: caches.default, waitUntil: (work) => ctx.waitUntil(work), maxAge: 60, staleFor: 600, key }, () => readTheCatalog(env.DB))
}
- 它发出
application/json带有弱的ETag(正文的SHA-256)并cache-control: public, max-age=…, stale-while-revalidate=…时max-age是保留答案的秒数。 请求If-None-Match名称当前 ETag( 微弱的比较, 列表, 或*得到304 没有一具尸体. - 虽然答案是新鲜的(在
maxAge旧的)build不运行。 过去的和内部staleFor保留答案 马上出去build在背景中运行一次waitUntil; 查找同样僵硬答案的请求 在15秒内,没有第二次续约。 无waitUntil固态的答案在发送前被更新. - 过去
maxAge + staleFor,或者没有保留任何东西, 请求等待build,它的错误被抛出,因为errorResponse如果build重现仍在其中的答案时抛出staleFor,这个出去onError听说了console.error(默认情况下). 拒绝的缓存put失败match从不失败的答案. key答案被保存在那里。 默认是请求自己的地址,所以?a=1&b=2财务报告和审定财务报表?b=2&a=1有两个答案. 无cache没有任何东西被保存着,每一个请求都建立起来.- 永远不要用它来回答因访客而异的答案(一个推车,一个会话,一个按账号计价的答案):
public,则 第一个访客的回答会到达下一个. - 仅限头部或头部
maxAge至少为1和astaleFor至少0整秒,并且一个值JSON可以 hold: 其它任何投出 TypeError, 因为这是一个编程错误 .
预览和制作
每个应用程序自行部署两本,预览和制作(tools/ci). answerCopy 财务报告和审定财务报表 markCopy 让每个
说出是哪一个,并把它排除在搜索结果之外:
import { answerCopy, type CopyEnv, markCopy } from '@codefusion-cc/workers-http'
export default {
fetch: async (request: Request, env: Env) => markCopy(answerCopy(request, env) ?? await answer(request, env), env),
}
answerCopy答案获取和标题/version.json与{ commit, environment }(单位:千美元)COMMIT,该部署 套装和APP_ENV从抄本"Wrangler vars";无所取用),并/robots.txt。所有其他请求 这是无效的,所以它保持应用的。 部署和小时生产表都读了.markCopy添加X-Robots-Tag: noindex, nofollow对每一个答案,和/robots.txt拒绝一切, 除非应用程序通过{ indexed: true }财务报告和审定财务报表APP_ENV没错production预览,缺少APP_ENV或者里面的打字没有搜索结果 一个WebSocket升级的通行证是它是什么。 网站的静态文件从不 以工人们所享受的_headers发送相同的信头.
饼干和大麻
getCookie(request, '__Host-session') // string, or null (missing or empty)
serializeCookie('__Host-session', token, { maxAge: 30 * 86_400 }) // Path=/; Max-Age=…; HttpOnly; Secure; SameSite=Lax
serializeCookie('__Host-session', '', { maxAge: 0 }) // clears it
await sha256(token) // 64 hex characters
await sha256(token, 'base64url') // 43 characters
serializeCookie 丢弃浏览器会拒绝或错误读取的内容: 带有空格、 引号、 逗号的值,
分号或反斜线(先编码), SameSite=None 或一个 __Host-编号 :__Secure- 无名称 secure页:1
__Host- 名称带有域或其它路径。 留一个 sha256 每被存储一列编码:两者不相匹配.
在应用测试中
import { testRateLimiter } from '@codefusion-cc/workers-http/testing'
const limiter = testRateLimiter({ limit: 2 }) // or { fail: new Error('down') }
await rateLimit(limiter, 'k'); await rateLimit(limiter, 'k')
await expect(rateLimit(limiter, 'k')).rejects.toMatchObject({ status: 429 })
limiter.keys // ['k', 'k', 'k']
import { testAssets } from '@codefusion-cc/workers-http/testing'
const assets = testAssets({ files: { '/index.html': '<!doctype html>…', '/sw.js': '…' } }) // or { fail: new Error('down') }
await worker.fetch(new Request('https://app.test/Mac/search/a+b'), { ...env, ASSETS: assets })
assets.paths // ['/Mac/search/a+b', '/']: what was asked; assets.requests holds the requests
testAssets 回答如静态资产 not_found_handling: "none": 路径文件和文件夹
index.html 在文件夹,一个307 来自任何其他拼写 两者,一个404 对于一个路径,没有文件, 405 对于其他
在文件上使用方法,在 ETag 上使用 304。 货包的工人测试 把它控制在真正的约束.
运行工人的测试 使用真正的约束 从Wrangler配置.