第61話で、イトはcloudを遠くの一台の貸しserverだと決めなかった。
source、output、queue、worker、access、scale ruleを分け、poster十二枚をpersistent shelfへ残した。
最後のoutput cardへ、五通のHTTP requestが届く。
release pageを開くrequest。
poster imageを取るrequest。
投票を送るrequest。
staff reportを開くrequest。
存在しないpathを試すrequest。
fixtureの公開まで、あと七十秒。
イトは一台のcloud boxへ、WEB SERVERと書いた札を置いた。
その前へ、HTML shelfを一つだけつなぐ。
「web serverはHTMLを返すmachineだよね。全部ここへ流せば速い」
イトはmethodも、authorityも、request-targetも見ず、すべてのrequestへ同じindex.htmlを返すbroad fallbackを開いた。
五つのstatus lampが、同時に200で光る。
しかしviewer screenは完成しなかった。
poster枠は破れたimage markになった。
投票buttonにはresponseを読めませんと出た。
staff用requestにも、public release pageが返った。
missing pathまで、存在するpageのように見えた。
statusはすべて200。
返事は、四つ間違っていた。
公開まで、あと四十二秒。
200が五つでも、正しいresponseとは限らない
イトはgreen lampを指さした。
ピコはlampへ触れず、各responseのContent-Typeとbody previewを開いた。
五つともtext/html。
五つとも同じbody。
イト
全部200なのに、どうして四つも壊れるの?
ピコ
statusだけでは、頼まれたrepresentationを返したか分からないよ
イト
web serverが返事をしたら、そこで成功じゃないの?
ピコ
どのrequestへ、どのrouteから、何を返したかまで一組で見よう
今回のlistener、route table、static shelf、application holder、staff boundary、response viewerは、外部systemから切り離した学習fixtureだ。
live server configuration、DNS、TLS certificate、credential、production route、public endpointは変更しない。
real account、personal data、real vote、staff reportの中身は使わない。
見るのは、requestのmethod、authority、request-targetと、responseのstatus、media type、body、selected routeだ。
web serverは、物理的な一台の名前ではない
RFC 9110 HTTP Semanticsは、HTTP serverをconnectionを受け、HTTP requestに対してHTTP responseを送るprogramとして定義する。
clientとserverは、そのconnectionでprogramが果たす役割だ。
同じprogramが、あるconnectionではserverになり、別のconnectionではclientになる場合もある。
だからweb serverを、特定の建物、一台のphysical machine、cloud instance一個と同じものだと決めない。
一台のmachineで複数のserver processが動く場合がある。
複数のmachineやworkerが一つのservice roleを担う場合もある。
edgeやcacheがresponseを返し、originまでrequestが届かない場合もある。
reverse proxyがrequestを受け、application serverへ渡す場合もある。
static fileを自分で読む場合もある。
大切なのは箱の名前ではなく、そのrequestを誰が受け、どのresponseを返す責任を持つかだ。
requestは「このpageください」だけではない
HTTP requestには、何をしたいかを示すmethodがある。
RFC 9110では、method tokenがrequest semanticsの第一のsourceになる。
GETは、target resourceのcurrent selected representationを求める。
POSTは、request contentをresource固有のsemanticsに従って処理してもらう。
同じpathでも、GETとPOSTを同じ操作だとは決めない。
request-targetは、methodを適用するtarget resourceを識別する。
RFC 9112 HTTP/1.1では、origin serverへ直接送る一般的なrequest-targetをabsolute pathとoptional queryからなるorigin-formとして示す。
さらにauthority、HTTP/1.1ならHost、HTTP/2 / HTTP/3なら:authorityを使い、同じserverが複数host nameのresourceを区別できる。
今回の五通は、こう違う。
GET launch.example /GET launch.example /assets/poster.avifPOST launch.example /api/voteGET staff.example /reportGET launch.example /missing
connection先が同じに見えても、method、authority、targetが違う。
一枚のHTMLへまとめてよい理由にはならない。
responseは、status・field・contentを持つ
HTTP responseにはstatus codeがある。
成功、redirect、client error、server error等のclassを伝える。
しかしstatus codeだけで、contentのformatや意味をすべて表すわけではない。
Content-Typeは、message contentまたはselected representationのmedia typeを示す。
text/html。
image/avif。
application/json。
recipientはmedia typeとmessage semanticsに従ってdataを処理する。
image decoderへHTML bodyを渡しても、status 200だけではimageにならない。
JSONを待つapplicationへHTMLを返しても、期待したcontractにはならない。
確認する単位は、少なくとも次の四つだ。
- statusは意図したclassか。
Content-Typeはbodyのformatと一致するか。- bodyはrequestが求めたrepresentationまたはresultか。
- requestは意図したroute / handlerへ届いたか。
staticを返すroute
あらかじめ用意したHTML、CSS、JavaScript、image、font等をstatic resourceとして返すrouteがある。
GET /assets/poster.avifに対応するobjectがあり、公開条件を満たすなら、image representationを返せる。
ただしpath文字列をfile system pathへそのまま結びつけるとは限らない。
build output、object storage、CDN、embedded resource等、storageの形は構成で変わる。
resourceがないなら、別のHTMLを200で返して存在するふりをせず、not foundとして扱うrouteを持てる。
directory traversal、private file exposure、content sniffing等のsecurity詳細はこの話では扱わない。
公開してよいresourceと、外から指定できるtargetのboundaryを別に持つ。
applicationへ渡すroute
投票、検索、login、checkout等では、requestをapplication logicへ渡す場合がある。
web-facing serverがrequestを受け、upstream applicationへforwardする。
applicationがdataを読み、resultを作る。
web-facing serverがそのresponseをclientへ返す。
このとき、最初にrequestを受けたprogramだけが処理を全部行ったとは限らない。
RFC 9110は、serverがtarget URIを判断した後、自分で処理する、別serverへforwardする、different resourceへredirectする、errorを返す、connectionをdropする等を選び得ると説明する。
どの選択を許すかはconfigurationとrequest contextで決まる。
upstreamが返事をしない場合を、static file missingと同じ原因にしない。
route not found、method not allowed、access denied、upstream failureを区別できれば、直す場所も変わる。
error responseも、役目のある返事
404 Not Foundは、origin serverがtarget resourceのcurrent representationを見つけられない、または存在を明かしたくないことを示すresponseだ。
Internet全体が壊れたという意味ではない。
405 Method Not Allowedは、target resourceは分かるが、そのmethodをsupportしないことを示す。
403 Forbiddenは、requestを理解したがfulfillを拒むresponseだ。
502 Bad Gatewayは、gateway / proxyとして動くserverがupstreamからinvalid responseを受けた場合に使われる。
どのstatusを返すべきかは、実際の状態とservice contractへ戻す。
すべてを200へ隠すと、clientは成功したと思って間違ったbodyを処理し、monitoringも原因を見失う。
一方で、status codeを細かくしただけでsecurityやobservabilityが完成するわけでもない。
public responseへsecret、internal path、stack traceを出さないboundaryが必要だ。
イトの前に、三つの選択肢が開く
公開まで、あと二十五秒。
A imageとJSONのpathにもHTMLをcopyする
file nameだけはそろう。
しかしmedia typeとbody contractは直らず、staff authorityとpublic authorityも分かれない。
B client側を全部HTMLとして読ませる
一時的にerror表示を消せるかもしれない。
しかしimage、vote、staff boundary、missing targetという違いをclientへ押しつける。
C broad fallbackを閉じ、requestの違いでrouteを分ける
method、authority、request-targetを先に読む。
static shelf、application holder、staff boundary、not-found responseをexplicitに分ける。
status、Content-Type、body、selected routeを同じviewerへ残す。
ピコはroute holderにもrelease buttonにも触れなかった。
「green lampを増やすんじゃない。違うお願いを、違う返事へ戻す」
イトはCを選んだ。
自分でall targets → index.html holderを閉じた。
五つのrequestを、一通ずつ再送した
イトはroute tableを五行に分けた。
GET launch.example /はstatic page holderへ。
GET launch.example /assets/poster.avifはpublic image holderへ。
POST launch.example /api/voteはfixture application holderへ。
GET staff.example /reportはpublic fixture connectionから拒否するboundaryへ。
unmatched targetはnot-found holderへ。
一度に全部を流さず、一通ずつ再送する。
一通目。
status 200。
Content-Type: text/html。
release pageのbody。
selected route static-page。
二通目。
status 200。
Content-Type: image/avif。
poster imageのbody。
selected route static-image。
三通目。
status 202。
Content-Type: application/json。
fixture receipt body。
selected route vote-application。
四通目。
status 403。
secretを含まない短いbody。
selected route staff-deny。
五通目。
status 404。
missing targetを示すbody。
selected route not-found。
poster枠にimageが入った。
投票buttonはreceiptを読めた。
staff reportの中身はpublic側へ出なかった。
missing pathは存在するpageのふりをしなかった。
expected responseは5。
observed responseは5。
media type mismatchは0。
unexpected 200は0。
public-to-staff route crossingは0。
unmatched target hidden by fallbackは0。

一つのresponseを見て、全routeの成功にしない
release pageが200になっても、image、application、staff boundary、not-foundが正しいとは限らない。
routeごとにrepresentative requestを持つ。
expected statusを持つ。
expected media typeを持つ。
body schemaまたはcontent identityを持つ。
selected route / upstreamを観察できるようにする。
変更前後で同じfixtureを流せば、「pageは開くがvoteだけ壊れた」「unknown pathまで200になった」を見つけやすい。
monitoringも一つのuptime lampだけにしない。
static response success。
application latency / error。
upstream reachability。
denied request。
not-found増加。
response size / media type anomaly。
service requirementに必要なsignalを分ける。
ただしlogへrequest bodyやcredentialを無制限に残さない。
observabilityにもdata minimization、retention、access boundaryが要る。
web serverの場所より、response boundaryを残す
workbenchには、六つのevidenceが残った。
expected response five。
observed response five。
media type mismatch zero。
unexpected success zero。
authority boundary crossing zero。
fallback-hidden target zero。
web serverを「cloudの中にあるHTML返却machine」と判断しない。HTTP requestを受け、responseを返すprogram上の役割として見る。method、authority、request-targetを読み、static representationを返すのか、applicationへ渡すのか、redirectやerrorを返すのかを明示する。結果はstatusだけでなく、Content-Type、body、selected routeを一組で確認する。
「web serverが返すのは、いつもpageじゃない。requestを読んで、返すべきresponseか、渡すべき先か、断る理由を選ぶんだ」
イトは五つのgreen lampを一つのsuccess badgeへ戻さなかった。
HTML、image、JSON、deny、not-foundのresponse cardを、五つのrequestの隣へ一枚ずつ置いた。
そのうちJSON cardだけが、小さく脈打つ。
bodyには、accepted: trueとreceipt idが入っていた。
pageの材料ではなく、dataそのものを返す窓口には、どんな約束が必要なのだろうか。
次回:APIは、JSONを返せば完成?
イトはJSON cardへ、APIと書いた札を貼ろうとした。
「JSONを返す入口なら、APIだよね」
ピコはformat cardへ触れず、request contract、response contract、error、version、authorizationのholderを照らした。
次回、ピコルート第63話。
イトは、APIをJSONが出る窓口だと決めなかった|何を約束する?

