Docsの目次

HTTP APIリファレンス

Nagi 0.2.0のAPIです。移行と認証・認可も参照してください。

import std.http.server as http
from std.http.server import Status as Code
このページの目次

MethodとStatus

http.Method.GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、CONNECT、TRACEを使えます。request.methodとの比較は借用し、Methodをコピーしません。request.is_getやrequest.is_postも使えます。http.method("PROPFIND")は拡張methodを検証して作ります。

Statusは数値を保持するコピー可能な値です。文字列への変換は比較や応答のために必要ありません。

用途 例
名前で指定 Code.OK、CREATED、NO_CONTENT、UNAUTHORIZED、FORBIDDEN、CONFLICT、TOO_MANY_REQUESTS
数値から作る try http.status(418)。最終応答として200〜599を検証する
数値を読む Code.NOT_FOUND.value → 404
説明を読む Code.NOT_FOUND.phrase → "Not Found"
分類する .is_success、.is_redirection、.is_client_error、.is_server_error

標準の2xx〜5xx定数を用意しています。.phraseは静的な文字列参照で、未登録の数値では空文字列です。1xxや101のprotocol切り替えは通常の最終応答には使えません。

Request

フィールド 型
method http.Method
path view[str]
query Option[view[str]]。?を除いた未変換のquery
body view[bytes]
is_get、is_postなど bool

path、query、bodyとヘッダーはRequest内のデータを借ります。参照を使用中にRequestをmoveできません。path登録は"/users/{id}"のような形にも対応します。captureの型付き取り出しAPIと接続元IPの取得APIはありません。本文は上限まで読み込んでからhandlerへ渡すため、受信ストリームではありません。

関数 戻り値
header(view(request), name) Result[Option[view[bytes]], Error]
header_text(view(request), name) Result[Option[view[str]], Error]
headers(view(request), name) Result[List[view[bytes]], Error]
is_json_content_type(view(request)) Result[bool, Error]
method_name(view(request.method)) view[str]

ヘッダー名は大文字・小文字を区別しません。単一取得で同名ヘッダーが複数ある場合はエラーです。headersはすべて返します。header_textはUTF-8を検証します。

is_json_content_typeはapplication/jsonを大文字・小文字を区別せずに判定し、前後の空白・タブとcharset=utf-8などのparameterを受け付けます。欠落や別のmedia typeはFalse、重複ヘッダーや不正な構文はErrです。application/problem+jsonは別のmedia typeです。parameterは構文だけを検証し、charsetで文字コードを切り替えません。JSON本文はUTF-8で読みます。この判定はヘッダーを借り、本文をコピーせず、成功時にメモリを確保しません。

Response

関数 戻り値
response_context(view(request)) view[ResponseContext]
empty(context, status) Result[Response, Error]
text(context, status, text) Result[Response, Error]
bytes(context, status, body) Result[Response, Error]
json[T](context, status, value) Result[Response, Error]
html_response(context, status, document) Result[Response, Error]
append_header(response, name, value: view[bytes]) Result[Response, Error]
append_header_text(response, name, value: view[str]) Result[Response, Error]

contextは現在のRequestからresponse_contextで借ります。応答予算のための不透明な値で、認証proofではありません。Requestより長く保持できず、defaultから作れません。factoryは予算の保持責任を持つ所有Responseを返し、明示した確保・直列化の上限を超えるとErrになります。tryで処理するかfactoryのResultを直接返し、handlerの業務エラーと予算・確保失敗を区別してください。

0.2.0 release CIは4 OSとbrowser受入に成功しました。下記の予算は記載した資源を制限しますが、process全体のメモリ、SLA、普遍的な確保失敗からの回復を保証しません。

text、bytesは借りた入力をコピーして、Responseが所有するbodyを作ります。jsonは値を借用してJSONを作るため、元の値をmoveしません。文字列リテラルはview引数へ直接渡せます。変数から借りる場合はview(value)を使います。

ヘッダー追加は応答を受け取り、新しい応答を返します。非reservedの同名ヘッダー(X-Traceなど)は重複を保持します。Content-Length/Transfer-Encoding/Content-Type/Set-Cookie/WWW-Authenticate/Cache-Control/Vary、CORS/CSP/nosniff等のsecurity-managedヘッダーは拒否します。Cookie/Sessionの発行は専用Session APIを使い、ヘッダー追加で迂回できません。HEADではbodyを送らず、GET相当の長さを保持します。204・205・304ではbodyを送信しません。authority-form CONNECTは起動設定節のgateで400になります。origin-form CONNECTのhandlerが成功応答を返した場合もtunnelは未対応で、501で接続を閉じます。

Appとroute

以下はroute登録部分の断片です。SF07の起動全体を表していません。下記の応答・rate予算と全routeのrate policy設定を加えてください。

app = http.app[State, AuthError](state, map_error)
app = try http.route(app, http.Method.GET, "/", http.public_policy[State](), handle)
app = try http.route_mapped(app, http.Method.POST, "/submit", http.public_policy[State](), submit, map_submit_error)
  • app[S, E](state, mapper)は状態を所有し、fn(view[ResponseContext], E) -> Result[Response, Error]を既定のエラー処理にします。
  • app_default[S](state)はError用の既定処理を使います。
  • Policyはpublic_policy[S]()、authenticated_policy[S](verifier)、authorized_policy[S,P](verifier, authorizer)から作ります。Aは順にunit、AuthScope、Grant[P]です。旧arityにはmigration診断が出ます。
  • handlerはasync def handle(request: Request, state: shared[S], access: A) -> Result[Response, E]です。状態そのものはリクエストごとにコピーしません。Rust側では状態にSend + Sync、handlerのFutureにSendなどが必要で、最終的な適合はbuildで検証します。
  • route_mappedはhandlerの独自エラー型と、それに合うmapperを指定できます。
  • 登録済みGETにHEADを自動で対応させます。HEADの明示登録を優先します。既存パスでmethodが違う場合は405とAllowを返します。

mapperは借用Contextを明示的に受け取り、アプリのエラーを処理します。mapper自身のErrは有限の組込fallbackへ進み、その独立予算まで不足した場合は再確保を繰り返さず接続を閉じます。応答できる場合はCORS/managed headersを維持します。常に応答できることや、OOMからの普遍的回復は保証しません。

mapperはアプリのエラーを処理します。不正なHTTP、制限超過、タイムアウトはサーバー側で処理します。データベースのエラー詳細や認証情報は応答へ直接出さないでください。

Nagi 0.1.10以降では、応答開始前のhandlerやmapperでunwindするpanicが起きた場合、詳細を含まない500を返し、その接続を閉じます。これはpanicの検出であり、共有状態やDBの更新を巻き戻す仕組みではありません。通常の失敗はResultで返してください。panic=abort、OOMなどによるプロセス終了、unwind中の二重panic、独自Rustの解放処理、応答開始後の障害は回復を保証しません。

エラー応答にリクエストIDなどが必要なら、handlerで検証した値を保持し、失敗時に独自エラー型へmoveできます。見積APIの例では、この方法で共通mapperへIDを渡しています。

SF07の明示予算とrate policy

応答・rateの新予算に暗黙defaultはありません。default_options()は従来のtransport設定を持ちますが、新予算を補いません。両方の予算と全routeの認証前rate policyを明示してから起動します。未設定や不整合はlisten前に拒否します。

設定API 契約
response_limits(body_bytes, user_header_bytes, user_header_count, managed_header_bytes, managed_header_count, serialization_writes, live_responses, retained_bytes, fallback_responses, fallback_retained_bytes, fallback_managed_header_bytes, fallback_managed_header_count) 12個のi64を明示し、Result[ResponseLimits, Error]を返す。通常応答とfallbackは別予算
with_response_limits(options, limits) OptionsとResponseLimitsをmoveし、Result[Options, Error]を返す
rate_limits(rules, domains, subject_slots, retained_bytes, request_work) 5個のi64を明示し、Result[RateLimits, Error]を返す。有限の件数・保持量・探索workを検査し、4096を固定の公開上限にしない
with_rate_limits(options, limits) OptionsとRateLimitsをmoveし、Result[Options, Error]を返す
rate_policy(burst, refill_ms) 認証前route bucketのResult[RatePolicy, Error]を返す
rate_subject(policy, burst, refill_ms, subject_slots, idle_ms) policyを消費して有限の認証済みsubject枠を追加し、Result[RatePolicy, Error]を返す
rate_route(app, method, path, policy) Appとpolicyをmoveし、state/業務エラーの型を保ったResult[App[S, E], Error]を返す

burstは最初に使えるtoken数と貯められる最大数、refill_msはtokenを1個補充する間隔です。例えばrate_policy(10, 1000)は最初の10件を受け入れ、その後は1秒ごとに1個を補充します。1秒ごとに10個を補充する設定ではありません。subject bucketも同じ規則です。subject枠の回収は全tokenの補充と明示idle条件の両方を満たした場合に限ります。サンプルの数値は有限な説明用設定で、実運用の推奨値や遅延SLAではありません。

採用した消費順序は入口検査→route枠→認証・CSRF・body検査→認証済みsubject枠→handler/Session発行・更新です。routeの429が401/403より先になる場合があります。Errや取消で返金しません。正常preflightはroute tokenを消費しませんが、入口のcapacity・同時実行数・期限の保護は受けます。subject枠は認証済み主体を得られるprofileだけで使います。同一route・同一process内の制限で、別route・別process・複数台を通じた全体制限ではありません。

token不足は429、table/quota容量不足やClosing/ClockFaultは503、内部故障やmapper自身の失敗は500です。既に成立した認証・期限等の拒否を上書きしません。独立した有限fallback枠まで不足した場合は接続を閉じます。

算術的に受理できる設定でも、全processメモリや遅延SLAの保証ではありません。下記の小規模構成の実測と配備先の条件を踏まえて予算を選んでください。

小規模構成の実測

公開APIのfixture sf07_small_profile.rsは接続・リクエスト各8枠、4 routeと、次の明示予算を使います。検証した構成例であり、暗黙defaultや実運用の推奨値ではありません。

予算 明示値
応答 body 4096 bytes、user header 4096 bytes/16件、managed header 4096 bytes/32件、直列化work 4096、同時応答8、通常保持枠2 MiB
fallback 応答8、独立保持枠2 MiB、managed header 4096 bytes/32件
rate rule 8、domain 8、subject枠1、保持枠1 MiB、request work 65536
route /ordinary・/headers・/healthyはburst 1000/refill 1000 ms、/limitedはburst 1/refill 86400000 ms

GitHub-hosted runnerでRust 1.99.1・固定した依存を使い、未最適化のtest profileで4回warmup後、loopback HTTP/1の32リクエストを逐次実行しました。時間にはclient・通信・解析・current-thread Tokio taskを含みます。CPU型番は記録しておらず、条件を揃えたOS間比較ではありません。

runner/architecture 中央値(ms) p95(ms) 32リクエスト中の呼出しthreadの累積確保bytes
Linux x86_64 0.450 0.489 1627520
Windows x86_64 0.681 0.737 1641088
macOS ARM64 0.183 0.212 1657152
macOS x86_64 0.487 0.553 1657216

根拠は2b1563dのLinux/macOS runと、967ad48eのWindows followupです。両snapshotでRust fixtureとproduction codeは同一です。followupはCLIテストの改行期待2箇所を修正し、最初のWindows runはnative実行前にそのテストで失敗しました。4構成ともheaderを多く持つ応答、rate 429後の正常route、停止後joinも確認しました。最終統合HEADの検証の代替ではありません。

allocation counterはloopback clientを含む呼出しthreadの累積eventで、解放数には測定前の確保を含む場合があります。物理的な保持量の最大値やprocess RSSの測定ではありません。論理的な課金境界と最後の所有者による解放は、既存のquota・native ownerの決定的テストで別に確認します。この測定や論理予算の受理から遅延SLAや普遍的なOOM回復を保証しません。

制限と停止

0.2.0では、serveとRustのserve_listenerにchecked authority設定が必要です。未設定のdefault_options()/options(...)だけでは起動Errになります。

起動設定 契約
authority(options, external_origin, authorities: List[str], entry_limit, byte_limit) HTTPS originと受理wire authorityの有限集合を検査し、Result[Options, Error]を返す。件数・bytesは正の有限値。空集合・canonical重複・不正origin/authority・上限超過はErr
trusted_proxy(options, peer_ips: List[str]) authority設定後だけ、同じ有限件数・bytes上限の実TCP peer IP ACLを設定。空・不正・unspecified/multicast・canonical重複はErr。CIDR/DNSではなく正確IP集合

originはhttps://host[:port]のみで、path/userinfo/query/fragmentは認めません。wire側のport省略と明示:443等は別entryです。DNSはASCIIの大小文字を正規化し、IPは標準IP parserを使います。IPv6はbracketが必要です。標準IPv4として解釈できない数字とdotだけのhostも拒否します。portはcanonical十進の1〜65535で、leading zero、userinfo、%/zone、末尾dot、Unicode、comma listを拒否します。IDNAは事前canonical ASCII A-labelを設定し、runtimeでUnicode変換やIDNA同値性の保証はしません。proxy peerだけは標準IP型でIPv4-mapped IPv6をIPv4へ正規化し、重複を拒否します。

authorityの再設定やproxy ACLの上書きはErrです。setterはOptionsをmoveし、成功Resultだけを起動へ渡します。失敗後に先の設定へ自動fallbackしません。

全route・404/405・body受信・verifier・proof発行より前に、HTTP/1.1のorigin-formと一個の非空Hostを検査します。不正・欠落・重複・非allowlist、absolute-form/authority-form/*、ForwardedまたはX-Forwarded-*のpresenceは400、HTTP/1.0は505です。これらruntime拒否はno-storeとcloseです。Hyper自身がservice前に拒否する不正HTTP構文・HTTP/2 prefaceは公式parserへ委譲し、handler/proofには届きません。標準listenerはHTTP/1のみです。

direct/proxyのprofileは起動時に固定します。proxyでもForwarded系はbackendへ送る前に除去し、Hostは登録済み内部値または登録済みpass-through値にします。ACLはacceptが返す実peerだけで判定し、headerからtrustやoriginを選びません。loopback ACLは同hostの別processを認証しません。Host一致はOrigin/CSRF/CORSを免除せず、下記の別契約として扱います。

serveはloopbackの平文HTTPで、origin設定はTLSを実装しません。ローカルのpublic応答確認とbrowser認証を区別してください。browser用は信頼された証明書のTLS frontendを置き、たとえばexternal origin https://localhost:8443、backend wire localhost:8080、peer 127.0.0.1を明示します。HTTP/2 frontendもHTTP/1.1へ終端します。Secure Cookieにlocalhost例外はありません。実TLS/browser・4 OSの検証は別の受入条件です。

設定 既定値 変更する関数
bodyの上限 1 MiB options(body_bytes, body_ms, handler_ms, shutdown_ms)
bodyの受信期限 10秒 options(...)
handlerの期限 2秒 options(...)
verifier+authorizerの共通期限 2秒 security_timeout(options, milliseconds)
停止時の待ち時間 10秒 options(...)
接続数/同時リクエスト 1024/256 capacity(options, connections, requests)
ヘッダー・次のリクエスト待ち 10秒 header_timeout(options, milliseconds)
応答の送信期限 10秒 send_timeout(options, milliseconds)
ヘッダーバッファ/件数 32 KiB/100件 header_limits(options, bytes, count)

securityの期限はverifierとauthorizerを合わせた一つの絶対予算です。security_timeoutは正のミリ秒のみ受理し、0や負数で無期限にはできません。認証後もbody・handlerの独立した期限は維持します。認証情報の期限が先に切れた場合、handlerへ渡す前に再検査します。

変更する関数はすべてResult[Options, Error]を返します。接続数と同時リクエスト数は受け付けの上限で、スレッド数ではありません。Ctrl+Cでは新しい接続を止め、処理中の接続を待ちます。停止期限後には接続taskを中止して回収します。実行開始済みのblocking処理は強制停止できません。

Nagi 0.1.10以降では、handlerの応答生成が正常に完了した時点で期限を過ぎていれば、504を返します。同期処理を期限時刻に強制停止する機能ではありません。処理が制御を戻すまで応答は遅れ、すでに行った状態変更も巻き戻しません。

本文上限を超えると、handlerを呼ばずに413とConnection: closeで拒否します。0.2.0開発版では応答の送信後、最大8 KiB・100 msと本文期限の短い方まで残りの入力を読み捨て、書込み側を閉じます。この間は接続枠を保持し、読み捨てた入力をhandlerへ渡しません。通常応答やidle Keep-Aliveではこの後片付けを行いません。残りの本文を最後まで読み捨てる実装ではなく、期限・上限・取消で切断します。未読本文を残した切断ではTCP resetが起こり得るため、すべてのOS・client・送信条件で413を受信できることは保証していません。

次の起動部分は、前の例で登録した2つのroute(/と/submit)へ続けます。必須のSF07予算と両routeのrate policyを含めています。数値は有限な設定方法の例で、実測済みの推奨値ではありません。handler・state・mapperの定義は別途必要です。全体をbuildできる例はHTTP認証サンプルを参照してください。

route_budget = try http.rate_policy(10, 1000)
app = try http.rate_route(app, http.Method.GET, "/", route_budget)
submit_budget = try http.rate_policy(10, 1000)
app = try http.rate_route(app, http.Method.POST, "/submit", submit_budget)
limits = try http.options(1048576, 10000, 2000, 10000)
limits = try http.capacity(limits, 8, 8)
limits = try http.header_limits(limits, 32768, 100)
limits = try http.authority(limits, "https://localhost", ["localhost:8080", "127.0.0.1:8080"], 2, 256)
response_budget = try http.response_limits(4096, 4096, 16, 4096, 32, 4096, 8, 2097152, 8, 2097152, 4096, 32)
limits = try http.with_response_limits(limits, response_budget)
rate_budget = try http.rate_limits(8, 8, 1, 1048576, 65536)
limits = try http.with_rate_limits(limits, rate_budget)
return await http.serve(app, 8080, limits)

実装の対応

NagiのAPIは標準moduleの登録とHTTP runtimeで実装しています。通信はHyperのHTTP/1、実行と接続taskの管理はTokio、pathの照合はmatchit、JSON変換はSerdeです。AxumのHTTP型も使いますが、このAPIのroute処理はAxum Routerではありません。

HTTP runtimeのテストには、実際のsocketを使う応答、制限、タイムアウト、接続終了の確認があります。Rust連携では独自のサーバーを作れますが、同じ制限や障害処理が自動で付くわけではありません。

明示CORS route metadata

CorsPolicyは所有するopaque設定で、認証proofではありません。CorsCredentials.OMITはAllow-Credentialsを省略し、ALLOWはtrueを出します。credentialの除去やPolicy/CSRFの免除はしません。cors_policy(origins: List[str], request_headers: List[str], credentials: CorsCredentials, max_age_seconds: i64, max_origins: i64, max_origin_bytes: i64, max_headers: i64, max_header_bytes: i64) -> Result[CorsPolicy, Error]で作成します。count/bytes上限は正のnative size、listは空も可能です。max-ageは明示した0..4294967295秒で、0はpositive lifetimeなしです。server側preflight cacheはありません。

cors_route[S,E](app: App[S,E], method: Method, registered_path: view[str], cors: CorsPolicy) -> Result[App[S,E], Error]はapp/configを消費し、既存の正確なmethod/登録patternへ一度だけ設定します。未知record・二重付与は失敗します。GET fallbackのHEADは継承し、明示HEAD/OPTIONSは別recordです。省略はcross-origin読取denyです。origin/header wildcardとCORS methodの*・CONNECT・TRACE・TRACKは拒否します。通常routeとframework管理routeで同じ設定操作を使います。

cors = try http.cors_policy(["https://app.example.invalid"], ["content-type"], http.CorsCredentials.ALLOW, 0, 2, 128, 2, 64)
app = try http.cors_route[State, Error](app, http.Method.GET, "/items/{id}", cors)

originはhttp/https scheme・ASCII host・effective portのcanonical exact一致です。DNS case/default port/IP表記は正規化します。null・wildcard・userinfo・Unicode・末尾DNS dot・空白・percent・path/query/fragment・複数Originは不可です。既存security用ExternalOriginはHTTPS専用のままです。返すgrantは設定済canonical値です。

OPTIONSにAccess-Control-Request-MethodまたはAccess-Control-Request-Headersがあればtarget routeのmetadataだけを使います。成功は空204と要求method/header・明示Max-Age・no-storeで、verifier/handler/proof/Session/DB処理なしです。通常OPTIONSは独自Policyが必要です。未知path/methodは404/405とgrantなしを保持します。重複/不正metadataは400、valid未許可origin/header・既定denyは403です。admission・宣言bodyの503/413を優先し、preflight bodyは400とcloseです。

actualの未許可/不正/欠落OriginはCORS grantなしで、通常認証・CSRF・status・handler動作を変えません。許可cross-origin preflightでもunsafe Session POSTの外部HTTPS Origin/token条件は免除されません。外側finalizerはCookie一回配送・no-store・CSP・nosniff・challenge・framingを保持し、選択済early errorでも有限typed Vary依存をunionします。認証/Sessionの304は秘密なし500、公的DTOの304は既存statusを保持します。CORSはSession CSRFとともに0.2.0に含まれます。release CIでTLS/browser/proxy受入を検証していますが、配備先のTLSとproxy設定は別に確認してください。

Nagi 0.2.0 · Docsこのページのソース