🍚

[RFC 9651]Structured Field Values おかわり おわかり(SFV, ABNF)

2024/10/03に公開

Intro

つい先日の2024年9月30日にStructured Field Values for HTTPがRFC 9651として新たに公開されました。

https://datatracker.ietf.org/doc/html/rfc9651

古いStructured Field Values for HTTP(RFC 8941)は廃止されます。
このRFC 9651は今までsfbisと呼ばれていたもので、bisは「アンコール」とか「おかわり」とかそういう意味で使われるらしいです。

Structured Field Values(本記事ではSFVと呼びます)についてググるとJxckさんの歴史的経緯に関する記事やyukiさんの変更内容に関する記事がいくつか出てくるのですが、それ以外では日本語記事をほとんど見かけず、単にHTTP Headerが難しくなっただけの印象を感じてしまいます。
そもそも今はHTTP Headerと呼ぶのはあまり正しくなく、HTTP FieldにおけるHeader Fieldと呼ぶのが正しいらしいところから始まるので、もはやわけがわかりません。

まあそれはそれとして、今回はSFVを少しでも理解したいので、RFC 9651の
ABNF#Appendix Cを整理して理解を深めていきたいと思います。

ABNF

まずはABNFを大きく2つに分けてみましょう。

sf-list       = list-member *( OWS "," OWS list-member )
list-member   = sf-item / inner-list
inner-list    = "(" *SP [ sf-item *( 1*SP sf-item ) *SP ] ")" parameters
parameters    = *( ";" *SP parameter )
parameter     = param-key [ "=" param-value ]
param-key     = key
key           = ( lcalpha / "*" ) *( lcalpha / DIGIT / "_" / "-" / "." / "*" )
lcalpha       = %x61-7A ; a-z
param-value   = bare-item
sf-dictionary = dict-member *( OWS "," OWS dict-member )
dict-member   = member-key ( parameters / ( "=" member-value ))
member-key    = key
member-value  = sf-item / inner-list
sf-item       = bare-item parameters
bare-item     = sf-integer / sf-decimal / sf-string / sf-token / sf-binary / sf-boolean / sf-date / sf-displaystring
sf-integer       = ["-"] 1*15DIGIT
sf-decimal       = ["-"] 1*12DIGIT "." 1*3DIGIT
sf-string        = DQUOTE *( unescaped / "%" / bs-escaped ) DQUOTE
sf-token         = ( ALPHA / "*" ) *( tchar / ":" / "/" )
sf-binary        = ":" base64 ":"
sf-boolean       = "?" ( "0" / "1" )
sf-date          = "@" sf-integer
sf-displaystring = "%" DQUOTE *( unescaped / "\" / pct-encoded ) DQUOTE
base64           = *( ALPHA / DIGIT / "+" / "/" ) *"="
unescaped        = %x20-21 / %x23-24 / %x26-5B / %x5D-7E
bs-escaped       = "\" ( DQUOTE / "\" )
pct-encoded      = "%" lc-hexdig lc-hexdig
lc-hexdig        = DIGIT / %x61-66 ; 0-9, a-f

前者がStructured Data Types#section-3で紹介されているトップレベル型にInner ListとParameterを含めたもので、後者がBare Itemと呼ばれるものです。

要約すると以下のとおりです。

  • HTTPフィールドには3つのトップレベル型があります。List(配列)、Dictionary(辞書)、Item(アイテム)です。
  • ListとDictionaryはコンテナで、そのメンバはItemかInner Lists(それ自体がItemのList)になります。
  • ItemもInner Listも、keyとvalueのペアでParameter化できます。

www.DeepL.com/Translator(無料版)で翻訳しました。一部改変しました。

また、Notational Conventions#section-1.2で紹介されているRFCのABNFも使用します。

OWS   = *( SP / HTAB )
tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA

OWSとtcharはRFC 9110のHTTP Semanticsから参照しています。

https://datatracker.ietf.org/doc/html/rfc9110

OWSとはOptional WhiteSpaceの略で、スペースを任意で追加できます。
要するにtext/plain; charset=utf-8text/plain;charset=utf-8どちらでもよいということです。
ここで注意したいのが、OWSは*SPとは異なり、スペースだけではなく水平タブ(本記事ではタブと呼びます)も含まれるケースがあります。

tcharはトークンで使えるCharacterのことです。
したがってsf-tokenでしか使われません。

VCHAR
SP
DIGIT
ALPHA
DQUOT
HTAB

VCHAR, SP, DIGIT, ALPHA, DQUOTEと、先程OWSで紹介したHTABはRFC 5234のAugmented BNF for Syntax Specificationsから参照しています。

https://datatracker.ietf.org/doc/html/rfc5234

ただ今回のABNF紹介ではこのまま記載することにします。
Characterに置き換えて書くとごちゃごちゃになってしまうので。
VCHARは印字可能なCharacter、SPはスペース、DIGITは0から9までの数値、ALPHAは大文字と小文字のアルファベット、DQUOTEはダブルクォート、HTABはタブです。

List

Listで使用されているABNFは以下のとおりです。

sf-list       = list-member *( OWS "," OWS list-member )
list-member   = sf-item / inner-list
OWS           = *( SP / HTAB )

OWSを置き換えて分かりやすくします。

sf-list       = list-member *( *( SP / HTAB ) "," *( SP / HTAB ) list-member )
list-member   = sf-item / inner-list

至ってシンプルですが、カンマの前後に任意でスペースとタブが含まれる点に注意です。
list-memberに含まれるItemとInner Listは後で説明します。

Dictionary

Dictionaryで使用されているABNFは以下のとおりです。

sf-dictionary = dict-member *( OWS "," OWS dict-member )
dict-member   = member-key ( parameters / ( "=" member-value ))
member-key    = key
key           = ( lcalpha / "*" ) *( lcalpha / DIGIT / "_" / "-" / "." / "*" )
lcalpha       = %x61-7A ; a-z
member-value  = sf-item / inner-list
parameters    = *( ";" *SP parameter )
OWS           = *( SP / HTAB )

おっと。
何だかよくわからなくなってきました。

先程のListのように2つに分けてみます。

sf-dictionary = dict-member *( *( SP / HTAB ) "," *( SP / HTAB ) dict-member )
dict-member   = ( %x61-7A ; a-z / "*" ) *( %x61-7A ; a-z / DIGIT / "_" / "-" / "." / "*" ) ( *( ";" *SP parameter ) / ( "=" sf-item / inner-list ))

lcalphaの%x61-7Aは小文字のアルファベットのことを指しているのでABNFのコメントを省略します。

sf-dictionary = dict-member *( *( SP / HTAB ) "," *( SP / HTAB ) dict-member )
dict-member   = ( %x61-7A / "*" ) *( %x61-7A / DIGIT / "_" / "-" / "." / "*" ) ( *( ";" *SP parameter ) / ( "=" sf-item / inner-list ))

とてもすっきりしました。
DictionaryそのもののABNFは先程と同様で、カンマの前後に任意でスペースとタブが含まれます。

ただdict-memberはlist-memberのように簡単にはいかず、keyの始まりは小文字のアルファベットかアスタリスク、2文字目からは数値やいくつか記号が使え、keyとvalueの間にはイコールが必要で、keyに紐づいたvalueはlist-memberのようにItemとInner Listがに含まれる必要があるようです。

また、keyの後にすぐイコールが来るパターンだけではなく、セミコロンが来るパターンもあり、この場合はParameterとして扱う必要があるようです。
セミコロンの後のスペースは任意です。

とりあえず理解できたので先へ進みます。

Item

Itemで使用されているABNFは以下のとおりです。

sf-item       = bare-item parameters
bare-item     = sf-integer / sf-decimal / sf-string / sf-token / sf-binary / sf-boolean / sf-date / sf-displaystring
parameters    = *( ";" *SP parameter )

面倒なので1つにしてしまいます。

sf-item       = sf-integer / sf-decimal / sf-string / sf-token / sf-binary / sf-boolean / sf-date / sf-displaystring *( ";" *SP parameter )

なるほど。
Bare ItemとParameterが使えることを理解できました。
つまりBare ItemはItemからParameterを引いたもの、と考えれば分かりやすいのかもしれません。

Inner List

Inner Listで使用されているABNFは以下のとおりです。

inner-list    = "(" *SP [ sf-item *( 1*SP sf-item ) *SP ] ")" parameters
parameters    = *( ";" *SP parameter )

これも1つにしてしまいます。

inner-list    = "(" *SP [ sf-item *( 1*SP sf-item ) *SP ] ")" *( ";" *SP parameter )

丸括弧の中に任意でItemを追加できるようです。
ItemがBare ItemとParameterであることは先程理解したので、Itemの前後にスペースが含まれること、ItemとItemの間には必ず1つ以上のスペースを含めなければならないことに注意すれば問題なさそうです。
ただ丸括弧でかこったItemのListにParameterを加えることができるため、ParameterがItemとInner Listどちらに付与されれているのか気をつけたほうがいいのかもしれません。

詳しくはInner Lists#section-3.1.1をご参照ください。

Parameter

Parameterで使用されているABNFは以下のとおりです。

parameter     = param-key [ "=" param-value ]
param-key     = key
key           = ( lcalpha / "*" ) *( lcalpha / DIGIT / "_" / "-" / "." / "*" )
lcalpha       = %x61-7A ; a-z
param-value   = bare-item
bare-item     = sf-integer / sf-decimal / sf-string / sf-token / sf-binary / sf-boolean / sf-date / sf-displaystring

1つにするとこう。

parameter     = ( %x61-7A ; a-z / "*" ) *( %x61-7A ; a-z / DIGIT / "_" / "-" / "." / "*" ) [ "=" sf-integer / sf-decimal / sf-string / sf-token / sf-binary / sf-boolean / sf-date / sf-displaystring ]

そしてlcalphaの%x61-7Aは小文字のアルファベットのことを指しているのでABNFのコメントを省略します。

parameter     = ( %x61-7A / "*" ) *( %x61-7A / DIGIT / "_" / "-" / "." / "*" ) [ "=" sf-integer / sf-decimal / sf-string / sf-token / sf-binary / sf-boolean / sf-date / sf-displaystring ]

keyはDictionaryと全く同じですが、valueがItemやInner Listではなく、Bare Itemであることに注意する必要があります。
またイコールとvalueは任意で追加するものなので、keyだけでもParameterは成立するようです。
注意しないといけない点がいくつかありますが、あくまでDictionaryのmenberやItem、Inner Listで表現できないものを表現するものなんですね。

詳しくはParameters#section-3.1.2をご参照ください。

Bare Item

Bare Itemはたくさんありますが、まずはRFC 8941の頃からある6つを紹介します。

sf-integer       = ["-"] 1*15DIGIT
sf-decimal       = ["-"] 1*12DIGIT "." 1*3DIGIT
sf-string        = DQUOTE *( unescaped / "%" / bs-escaped ) DQUOTE
sf-token         = ( ALPHA / "*" ) *( tchar / ":" / "/" )
sf-binary        = ":" base64 ":"
sf-boolean       = "?" ( "0" / "1" )
base64           = *( ALPHA / DIGIT / "+" / "/" ) *"="
unescaped        = %x20-21 / %x23-24 / %x26-5B / %x5D-7E
bs-escaped       = "\" ( DQUOTE / "\" )
tchar            = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA

Bare Itemだけにします。

sf-integer       = ["-"] 1*15DIGIT
sf-decimal       = ["-"] 1*12DIGIT "." 1*3DIGIT
sf-string        = DQUOTE *( %x20-21 / %x23-24 / %x26-5B / %x5D-7E / "%" / "\" ( DQUOTE / "\" ) ) DQUOTE
sf-token         = ( ALPHA / "*" ) *( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA / ":" / "/" )
sf-binary        = ":" *( ALPHA / DIGIT / "+" / "/" ) *"=" ":"
sf-boolean       = "?" ( "0" / "1" )

めっちゃ見やすくなった。

上から順番に整数、小数、文字列、トークン、バイナリ、真偽です。

文字列で使えるCharacterが分かりづらいですが、ダブルクォートでかこった中でSPとVCHARが使えるものの、VCHARに含まれるダブルクォートとバックスラッシュの前にはバックスラッシュを付ける必要があることを示しています。
また、後で説明するDisplay Stringでパーセントを使う関係で%x23-5Bからパーセント(%x25)を取り除き、別途パーセントを記載しているのがRFC 8941のABNFと異なるので注意しましょう。

Date

RFC 9651で追加された日付です。

sf-date          = "@" sf-integer
sf-integer       = ["-"] 1*15DIGIT

はい。

sf-date          = "@" ["-"] 1*15DIGIT

最初にアットマークをつけて、その後にsf-integerです。
sf-integerを使用しているので、マイナス表記にもできますし、何よりUnixtimeそのまま使えるのでメール由来のInternet Message Format(RFC 5322)に基づいたフォーマットとかタイムゾーンとか考えなくてよいのが最高です。

https://datatracker.ietf.org/doc/html/rfc5322

ただこれがDate Header Fieldですと言われたら信じる人はいるんですかね。

Display String

RFC 9651で追加された表示文字列です(Display Stringの日本語訳がこれでよいのかは不明)

sf-displaystring = "%" DQUOTE *( unescaped / "\" / pct-encoded ) DQUOTE
unescaped        = %x20-21 / %x23-24 / %x26-5B / %x5D-7E
pct-encoded      = "%" lc-hexdig lc-hexdig
lc-hexdig        = DIGIT / %x61-66 ; 0-9, a-f

Bare Itemだけにするとこう。

sf-displaystring = "%" DQUOTE *( %x20-21 / %x23-24 / %x26-5B / %x5D-7E / "\" / "%" DIGIT / %x61-66 ; 0-9, a-f DIGIT / %x61-66 ; 0-9, a-f ) DQUOTE

そしてlc-hexdigは小文字の16進数を指しているので、ABNFのコメントを省略します。

sf-displaystring = "%" DQUOTE *( %x20-21 / %x23-24 / %x26-5B / %x5D-7E / "\" / "%" DIGIT / %x61-66 DIGIT / %x61-66 ) DQUOTE

まず最初にパーセントをつけて、その後に文字列です。
ただこの文字列はsf-stringと少し異なります。

ダブルクォートでかこった中でSPとVCHARが使えるまではだいたい同じなのですが、VCHARに含まれるバックスラッシュは前にバックスラッシュを付けることなく使用でき、VCHARに含まれていなくても印字可能なCharacterであればパーセントエンコーディングすることで使えるようになっています。
つまり私達が普段query-stringで日本語をパーセントエンコーディングしているように、日本語をはじめとしたUTF-8文字列をパーセントエンコーディングして表示できるようにしたものがDisplay Stringということです。

ただsf-stringと異なり、VCHARに含まれるパーセントとダブルクォートはそのまま使用できないため、こちらもパーセントエンコーディングしてあげないといけない点には注意です。

Outro

ここまで書いてきて、結局のところJxckさんとyukiさんが書いている内容と同じことしか書いていないのでは……と思いましたが、せっかくなので公開したいと思います。
ABNFをまとめることで理解も深まったので。
後でもう少し分かりやすく書き直したいですね。

あとThe Link-Template HTTP Header Field(RFC 9652)とRetrofit Structured Fields for HTTPについても書きたい……。

https://datatracker.ietf.org/doc/html/rfc9652
https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-retrofit-06
https://github.com/httpwg/http-extensions/issues/2521

Ref

https://blog.jxck.io/entries/2021-01-31/structured-field-values.html
https://blog.jxck.io/entries/2023-05-17/abnf-or-algorithm-in-rfc.html
https://asnokaze.hatenablog.com/entry/2023/02/28/011900
https://asnokaze.hatenablog.com/entry/2024/01/31/012828
https://shogo82148.github.io/blog/2024/08/13/structured-field-values/
https://github.com/httpwg/http-extensions/issues?q=label%3Aheader-structure+sort%3Aupdated-desc

Appendix

2024年10月30日追記

AppendixやIANAやHTTPWGを見ても一部RFCのSFVのトップレベル型が書いてないことがたまにあったりしたので一覧を残しておきます(WHATWGやDraftなどは除く)

RFC8942-List: Accept-CH
RFC9209-List: Proxy-Status
RFC9211-List: Cache-Status
RFC9213-Dict: CDN-Cache-Control
RFC9218-Dict: Priority
RFC9297-Item: Capsule-Protocol
RFC9421-Dict: Accept-Signature
RFC9421-Dict: Signature
RFC9421-Dict: Signature-Input
RFC9440-Item: Client-Cert
RFC9440-List: Client-Cert-Chain
RFC9530-Dict: Content-Digest
RFC9530-Dict: Repr-Digest
RFC9530-Dict: Want-Content-Digest
RFC9530-Dict: Want-Repr-Digest
RFC9652-List: Link-Template

最近できたRFCだとRFC9449のDPoPとDPoP-NonceはSFV使用していない。

Discussion