diff --git a/lib/hpax/table.ex b/lib/hpax/table.ex index 8873b45..e5bc92b 100644 --- a/lib/hpax/table.ex +++ b/lib/hpax/table.ex @@ -25,7 +25,7 @@ defmodule HPAX.Table do } @static_table [ - {":authority", nil}, + {":authority", ""}, {":method", "GET"}, {":method", "POST"}, {":path", "/"}, @@ -39,53 +39,53 @@ defmodule HPAX.Table do {":status", "400"}, {":status", "404"}, {":status", "500"}, - {"accept-charset", nil}, + {"accept-charset", ""}, {"accept-encoding", "gzip, deflate"}, - {"accept-language", nil}, - {"accept-ranges", nil}, - {"accept", nil}, - {"access-control-allow-origin", nil}, - {"age", nil}, - {"allow", nil}, - {"authorization", nil}, - {"cache-control", nil}, - {"content-disposition", nil}, - {"content-encoding", nil}, - {"content-language", nil}, - {"content-length", nil}, - {"content-location", nil}, - {"content-range", nil}, - {"content-type", nil}, - {"cookie", nil}, - {"date", nil}, - {"etag", nil}, - {"expect", nil}, - {"expires", nil}, - {"from", nil}, - {"host", nil}, - {"if-match", nil}, - {"if-modified-since", nil}, - {"if-none-match", nil}, - {"if-range", nil}, - {"if-unmodified-since", nil}, - {"last-modified", nil}, - {"link", nil}, - {"location", nil}, - {"max-forwards", nil}, - {"proxy-authenticate", nil}, - {"proxy-authorization", nil}, - {"range", nil}, - {"referer", nil}, - {"refresh", nil}, - {"retry-after", nil}, - {"server", nil}, - {"set-cookie", nil}, - {"strict-transport-security", nil}, - {"transfer-encoding", nil}, - {"user-agent", nil}, - {"vary", nil}, - {"via", nil}, - {"www-authenticate", nil} + {"accept-language", ""}, + {"accept-ranges", ""}, + {"accept", ""}, + {"access-control-allow-origin", ""}, + {"age", ""}, + {"allow", ""}, + {"authorization", ""}, + {"cache-control", ""}, + {"content-disposition", ""}, + {"content-encoding", ""}, + {"content-language", ""}, + {"content-length", ""}, + {"content-location", ""}, + {"content-range", ""}, + {"content-type", ""}, + {"cookie", ""}, + {"date", ""}, + {"etag", ""}, + {"expect", ""}, + {"expires", ""}, + {"from", ""}, + {"host", ""}, + {"if-match", ""}, + {"if-modified-since", ""}, + {"if-none-match", ""}, + {"if-range", ""}, + {"if-unmodified-since", ""}, + {"last-modified", ""}, + {"link", ""}, + {"location", ""}, + {"max-forwards", ""}, + {"proxy-authenticate", ""}, + {"proxy-authorization", ""}, + {"range", ""}, + {"referer", ""}, + {"refresh", ""}, + {"retry-after", ""}, + {"server", ""}, + {"set-cookie", ""}, + {"strict-transport-security", ""}, + {"transfer-encoding", ""}, + {"user-agent", ""}, + {"vary", ""}, + {"via", ""}, + {"www-authenticate", ""} ] @static_table_size length(@static_table) @@ -149,10 +149,10 @@ defmodule HPAX.Table do Looks up a header by index `index` in the given `table`. Returns `{:ok, {name, value}}` if a header is found at the given `index`, otherwise returns - `:error`. `value` can be a binary in case both the header name and value are present in the - table, or `nil` if only the name is present (this can only happen in the static table). + `:error`. Some static table entries (see RFC 7541, Appendix A) have no defined value; `value` + is the empty binary `""` for those. """ - @spec lookup_by_index(t(), pos_integer()) :: {:ok, {binary(), binary() | nil}} | :error + @spec lookup_by_index(t(), pos_integer()) :: {:ok, {binary(), binary()}} | :error def lookup_by_index(table, index) # Static table @@ -190,7 +190,7 @@ defmodule HPAX.Table do > header field names MUST be converted to lowercase prior to their encoding in HTTP/2 """ - @spec lookup_by_header(t(), binary(), binary() | nil) :: + @spec lookup_by_header(t(), binary(), binary()) :: {:full, pos_integer()} | {:name, pos_integer()} | :not_found def lookup_by_header(table, name, value) diff --git a/test/hpax/table_test.exs b/test/hpax/table_test.exs index f7cfd0d..83673a3 100644 --- a/test/hpax/table_test.exs +++ b/test/hpax/table_test.exs @@ -13,17 +13,20 @@ defmodule HPAX.TableTest do # These are in the static table. assert {:full, _} = Table.lookup_by_header(table, ":status", "200") - assert {:name, _} = Table.lookup_by_header(table, ":authority", nil) + # :authority has no defined value in the static table (RFC 7541, Appendix A), which is + # represented as the empty binary, so it's a full match against "" and a name-only match + # against anything else. + assert {:full, _} = Table.lookup_by_header(table, ":authority", "") assert {:name, _} = Table.lookup_by_header(table, ":authority", "https://example.com") - assert Table.lookup_by_header(table, "my-nonexistent-header", nil) == :not_found + assert Table.lookup_by_header(table, "my-nonexistent-header", "") == :not_found assert Table.lookup_by_header(table, "my-nonexistent-header", "my-value") == :not_found table = Table.add(table, ":my-header", "my-value") assert {:full, _} = Table.lookup_by_header(table, ":my-header", "my-value") assert {:name, _} = Table.lookup_by_header(table, ":my-header", "other-value") - assert {:name, _} = Table.lookup_by_header(table, ":my-header", nil) + assert {:name, _} = Table.lookup_by_header(table, ":my-header", "") end test "LRU eviction" do @@ -50,7 +53,7 @@ defmodule HPAX.TableTest do end test "with an index in the static table" do - assert Table.lookup_by_index(Table.new(100, :never), 1) == {:ok, {":authority", nil}} + assert Table.lookup_by_index(Table.new(100, :never), 1) == {:ok, {":authority", ""}} end test "with an index in the dynamic table" do diff --git a/test/hpax_test.exs b/test/hpax_test.exs index 5d1da24..07f7d57 100644 --- a/test/hpax_test.exs +++ b/test/hpax_test.exs @@ -29,6 +29,17 @@ defmodule HPAXTest do assert headers == [{"custom-key", "custom-header"}] end + test "decode/2 of a fully-indexed reference to a static entry with no defined value returns an empty binary, not nil" do + table = HPAX.new(1000) + + # Index 58 is "user-agent" (RFC 7541, Appendix A), which has no defined default value. + # A peer is allowed to reference it via a plain Indexed Header Field Representation + # (section 6.1); the decoded value must be a binary per this module's documented contract, + # not the atom `nil`. + assert {:ok, [{"user-agent", value}], %HPAX.Table{}} = HPAX.decode(<<0xBA>>, table) + assert value == "" + end + # https://http2.github.io/http2-spec/compression.html#rfc.section.C.3.1 test "encode/2 with a literal example from the spec" do table = HPAX.new(1000, huffman_encoding: :never) @@ -49,6 +60,17 @@ defmodule HPAXTest do assert IO.iodata_to_binary(encoded) == expected end + test "encode/2 emits a compact indexed reference for a header whose value matches a static entry with no defined default value" do + table = HPAX.new(1000, huffman_encoding: :never) + + # Index 58 is "user-agent" (RFC 7541, Appendix A), which has no defined default value in + # the static table. Encoding it with a literal "" value should still find a full match + # there and produce a single indexed-header-field byte (section 6.1), rather than falling + # back to a longer literal representation. + assert {encoded, %HPAX.Table{}} = HPAX.encode([{:store, "user-agent", ""}], table) + assert IO.iodata_to_binary(encoded) == <<0xBA>> + end + # https://http2.github.io/http2-spec/compression.html#rfc.section.C.4.1 test "encode/2 with a Huffman example from the spec" do table = HPAX.new(1000, huffman_encoding: :always) @@ -289,7 +311,7 @@ defmodule HPAXTest do defp header() do header_from_static_table = bind(member_of(HPAX.Table.__static_table__()), fn - {name, nil} -> {constant(name), binary()} + {name, ""} -> {constant(name), binary()} {name, value} -> constant({name, value}) end)