[インデックス 17793] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおける Content-Type
ヘッダの処理に関する改善です。具体的には、HTTPレスポンスの Content-Type
ヘッダが明示的に設定されていない場合に、ファイルの内容から Content-Type
を推測(スニッフィング)する挙動を制御するための変更が加えられています。これにより、開発者が Content-Type
を意図的に空にしたい場合に、ライブラリが勝手に推測して設定してしまう問題を解決します。
コミット
commit 21e6b90d36db8d10e93ca281aee404b5f7720f48
Author: Michael Piatek <piatek@google.com>
Date: Tue Oct 15 08:22:04 2013 +1100
net/http: skip content-type sniffing if the header is explicitly unset.
Fixes #5953
R=dsymonds, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/14434044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/21e6b90d36db8d10e93ca281aee404b5f7720f48
元コミット内容
net/http: skip content-type sniffing if the header is explicitly unset.
Fixes #5953
R=dsymonds, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/14434044
変更の背景
この変更は、Goの net/http
パッケージが提供するファイルサービング機能(ServeFile
や ServeContent
など)において、HTTPレスポンスの Content-Type
ヘッダの挙動に関する問題を修正するために導入されました。
従来の net/http
パッケージでは、レスポンスヘッダに Content-Type
が設定されていない場合、Goは自動的にファイルの内容や拡張子に基づいて Content-Type
を推測(スニッフィング)し、ヘッダに設定していました。これは多くの場合において便利な機能ですが、開発者が意図的に Content-Type
ヘッダを空にしたい、あるいは特定の状況下で Content-Type
の自動推測を避けたい場合に問題を引き起こしていました。
具体的には、GoのIssue #5953で報告された問題が背景にあります。このIssueでは、Content-Type
ヘッダを明示的に空の文字列として設定しても、net/http
がそれを無視してファイルの内容から Content-Type
を推測し、ヘッダを上書きしてしまうという挙動が指摘されました。例えば、セキュリティ上の理由から Content-Type
を意図的に非公開にしたい場合や、カスタムの Content-Type
処理ロジックを持つ場合に、この自動スニッフィングが邪魔になることがありました。
このコミットは、このような開発者の意図を尊重し、Content-Type
ヘッダが明示的に空の文字列として設定された場合には、自動的なスニッフィングを行わないように挙動を変更することで、より柔軟な制御を可能にすることを目的としています。
前提知識の解説
HTTP Content-Type ヘッダ
HTTPの Content-Type
ヘッダは、HTTPメッセージのエンティティボディに含まれるデータのメディアタイプ(MIMEタイプ)を示すために使用されます。これにより、クライアント(ブラウザなど)は受信したデータの種類を理解し、適切に処理(表示、再生など)することができます。
例:
text/html
: HTMLドキュメントapplication/json
: JSONデータimage/png
: PNG画像text/plain; charset=utf-8
: UTF-8エンコードされたプレーンテキスト
Content-Type スニッフィング
Content-Type スニッフィング(MIMEスニッフィングとも呼ばれる)は、HTTPレスポンスに Content-Type
ヘッダが指定されていない場合や、指定された Content-Type
が不明確な場合に、ブラウザやその他のクライアントが受信したデータのバイト列を検査して、その実際のメディアタイプを推測するプロセスです。
これは、ウェブサーバーが常に正しい Content-Type
を設定するとは限らないため、ユーザーエクスペリエンスを向上させるために導入されました。しかし、悪意のあるユーザーがアップロードしたファイルが、意図しない Content-Type
として解釈され、クロスサイトスクリプティング(XSS)などのセキュリティ脆弱性を引き起こす可能性もあります。そのため、サーバー側で X-Content-Type-Options: nosniff
ヘッダを設定して、ブラウザのスニッフィングを抑制することが推奨される場合もあります。
Goの net/http
パッケージでは、ServeFile
や ServeContent
関数が、デフォルトで Content-Type
ヘッダが設定されていない場合に、ファイルの内容を読み取って Content-Type
を推測する機能を持っています。このコミットは、この自動スニッフィングの挙動をより細かく制御できるようにするためのものです。
Go言語の net/http
パッケージ
net/http
パッケージは、Go言語でHTTPクライアントおよびサーバーを実装するための基本的な機能を提供します。これには、HTTPリクエストの処理、レスポンスの生成、ルーティング、ミドルウェアのサポートなどが含まれます。
http.ResponseWriter
: HTTPレスポンスを構築するためのインターフェース。ヘッダの設定やボディの書き込みを行います。http.Request
: 受信したHTTPリクエストを表す構造体。http.ServeFile(w ResponseWriter, r *Request, name string)
: 指定されたパスのファイルをHTTPレスポンスとして提供するヘルパー関数。http.ServeContent(w ResponseWriter, r *Request, name string, modtime time.Time, content io.ReadSeeker)
:ServeFile
よりも低レベルで、io.ReadSeeker
インターフェースを実装する任意のコンテンツを提供できる関数。
Goの Header
マップと Get
メソッド
Goの http.Header
は map[string][]string
型であり、HTTPヘッダのキーと値のペアを管理します。
w.Header().Get("Content-Type")
: ヘッダマップから指定されたキーの最初の値を取得します。キーが存在しない場合や、値が空のスライスの場合、空文字列""
を返します。w.Header()["Content-Type"]
: ヘッダマップから直接スライス[]string
を取得します。この方法では、キーが存在するかどうか(haveType
)と、スライスが空かどうかを区別できます。
この違いが、今回のコミットの核心部分となります。Get
メソッドでは、ヘッダが存在しない場合と、ヘッダが空のスライスとして明示的に設定されている場合を区別できませんでした。
技術的詳細
このコミットの主要な変更点は、net/http
パッケージ内の serveContent
関数と response
構造体の needsSniff
メソッドにおける Content-Type
ヘッダのチェックロジックです。
serveContent
関数の変更
serveContent
関数は、ファイルやその他のコンテンツをHTTPレスポンスとして提供する際に、Content-Type
ヘッダの処理を担当します。
変更前:
// If Content-Type isn't set, use the file's extension to find it.
ctype := w.Header().Get("Content-Type")
if ctype == "" {
// ... Content-Type sniffing logic ...
w.Header().Set("Content-Type", ctype)
}
変更前は、w.Header().Get("Content-Type")
の結果が空文字列 ""
であれば、Content-Type
が設定されていないと判断し、スニッフィングを行っていました。しかし、Get
メソッドは、ヘッダが全く存在しない場合と、w.Header()["Content-Type"] = []string{}
のように明示的に空のスライスとして設定された場合の両方で ""
を返します。このため、開発者が Content-Type
を意図的に空にしたい場合でも、スニッフィングが実行されてしまっていました。
変更後:
// If Content-Type isn't set, use the file's extension to find it, but
// if the Content-Type is unset explicitly, do not sniff the type.
ctypes, haveType := w.Header()["Content-Type"]
var ctype string
if !haveType {
// ... Content-Type sniffing logic ...
w.Header().Set("Content-Type", ctype)
} else if len(ctypes) > 0 {
ctype = ctypes[0]
}
変更後では、w.Header().Get("Content-Type")
の代わりに、ctypes, haveType := w.Header()["Content-Type"]
を使用して、Content-Type
ヘッダがマップ内に存在するかどうか(haveType
)を直接チェックしています。
!haveType
:Content-Type
ヘッダが全く設定されていない場合。この場合にのみ、ファイルの内容や拡張子に基づくContent-Type
のスニッフィングが行われます。haveType
かつlen(ctypes) == 0
:Content-Type
ヘッダが明示的に空のスライス(例:w.Header()["Content-Type"] = []string{}
)として設定されている場合。この場合、!haveType
はfalse
となり、スニッフィングはスキップされます。これにより、開発者がContent-Type
を意図的に空に保つことができるようになります。haveType
かつlen(ctypes) > 0
:Content-Type
ヘッダが既に値を持って設定されている場合。この場合もスニッフィングは行われず、既存の値が使用されます。
response.needsSniff
メソッドの変更
response.needsSniff
メソッドは、HTTPレスポンスの書き込み中に Content-Type
のスニッフィングが必要かどうかを判断するために使用されます。
変更前:
func (w *response) needsSniff() bool {
return !w.cw.wroteHeader && w.handlerHeader.Get("Content-Type") == "" && w.written < sniffLen
}
ここでも w.handlerHeader.Get("Content-Type") == ""
が使用されており、Content-Type
が存在しない場合と明示的に空の場合を区別できませんでした。
変更後:
func (w *response) needsSniff() bool {
_, haveType := w.handlerHeader["Content-Type"]
return !w.cw.wroteHeader && !haveType && w.written < sniffLen
}
変更後では、w.handlerHeader.Get("Content-Type") == ""
の代わりに !haveType
を使用しています。これにより、Content-Type
ヘッダがマップ内に存在しない場合にのみ needsSniff
が true
を返すようになり、明示的に空に設定された場合にはスニッフィングが抑制されます。
テストの追加と修正
この変更の挙動を検証するために、fs_test.go
と response_test.go
にテストが追加・修正されています。
TestServeFileContentType
では、Content-Type
を明示的に空に設定した場合にスニッフィングが行われないことを確認する新しいテストケースが追加されました。TestNeedsSniff
では、Content-Type
がnil
(つまり、w.Header()["Content-Type"] = nil
のように設定された場合)のときにneedsSniff
がfalse
を返すことを確認するテストが追加されました。これは、Content-Type
ヘッダがマップ内に存在しない場合(!haveType
)と、キーは存在するが値がnil
の場合(haveType
はtrue
だがlen(ctypes)
は0
)の挙動を明確にするものです。ただし、w.Header()["Content-Type"] = nil
はw.Header().Del("Content-Type")
と同等であり、ヘッダがマップから削除されるため、haveType
はfalse
になります。したがって、このテストはneedsSniff
の変更後のロジックと整合しています。
コアとなるコードの変更箇所
src/pkg/net/http/fs.go
--- a/src/pkg/net/http/fs.go
+++ b/src/pkg/net/http/fs.go
@@ -140,9 +140,11 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
code := StatusOK
- // If Content-Type isn't set, use the file's extension to find it.
- ctype := w.Header().Get("Content-Type")
- if ctype == "" {
+ // If Content-Type isn't set, use the file's extension to find it, but
+ // if the Content-Type is unset explicitly, do not sniff the type.
+ ctypes, haveType := w.Header()["Content-Type"]
+ var ctype string
+ if !haveType {
ctype = mime.TypeByExtension(filepath.Ext(name))
if ctype == "" {
// read a chunk to decide between utf-8 text and binary
@@ -156,6 +158,8 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,\
}
}
w.Header().Set("Content-Type", ctype)
+ } else if len(ctypes) > 0 {
+ ctype = ctypes[0]
}
size, err := sizeFunc()
src/pkg/net/http/fs_test.go
--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -20,6 +20,7 @@ import (
"os/exec"
"path"
"path/filepath"
+ "reflect"
"regexp"
"runtime"
"strconv"
@@ -319,24 +320,29 @@ func TestServeFileContentType(t *testing.T) {
defer afterTest(t)
const ctype = "icecream/chocolate"
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
- if r.FormValue("override") == "1" {
+ switch r.FormValue("override") {
+ case "1":
w.Header().Set("Content-Type", ctype)
+ case "2":
+ // Explicitly inhibit sniffing.
+ w.Header()["Content-Type"] = []string{}
}
ServeFile(w, r, "testdata/file")
}))
defer ts.Close()
- get := func(override, want string) {
+ get := func(override string, want []string) {
resp, err := Get(ts.URL + "?override=" + override)
if err != nil {
t.Fatal(err)
}
- if h := resp.Header.Get("Content-Type"); h != want {
- t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
+ if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
+ t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
}
resp.Body.Close()
}
- get("0", "text/plain; charset=utf-8")
- get("1", ctype)
+ get("0", []string{"text/plain; charset=utf-8"})
+ get("1", []string{ctype})
+ get("2", nil)
}
func TestServeFileMimeType(t *testing.T) {
src/pkg/net/http/response_test.go
--- a/src/pkg/net/http/response_test.go
+++ b/src/pkg/net/http/response_test.go
@@ -614,3 +614,16 @@ func TestResponseContentLengthShortBody(t *testing.T) {\n \t\tt.Errorf("io.Copy error = %#v; want io.ErrUnexpectedEOF", err)\n \t}\n }\n+\n+func TestNeedsSniff(t *testing.T) {\n+\t// needsSniff returns true with an empty response.\n+\tr := &response{}\n+\tif got, want := r.needsSniff(), true; got != want {\n+\t\tt.Errorf("needsSniff = %t; want %t", got, want)\n+\t}\n+\t// needsSniff returns false when Content-Type = nil.\n+\tr.handlerHeader = Header{"Content-Type": nil}\n+\tif got, want := r.needsSniff(), false; got != want {\n+\t\tt.Errorf("needsSniff empty Content-Type = %t; want %t", got, want)\n+\t}\n+}\n```
### `src/pkg/net/http/server.go`
```diff
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -341,7 +341,8 @@ func (w *response) requestTooLarge() {\n \n // needsSniff reports whether a Content-Type still needs to be sniffed.\n func (w *response) needsSniff() bool {\n-\treturn !w.cw.wroteHeader && w.handlerHeader.Get("Content-Type") == "" && w.written < sniffLen\n+\t_, haveType := w.handlerHeader["Content-Type"]\n+\treturn !w.cw.wroteHeader && !haveType && w.written < sniffLen\n }\n \n // writerOnly hides an io.Writer value's optional ReadFrom method
コアとなるコードの解説
src/pkg/net/http/fs.go
の serveContent
関数
この変更の最も重要な部分は、serveContent
関数における Content-Type
の決定ロジックです。
ctypes, haveType := w.Header()["Content-Type"]
: ここが変更の核心です。w.Header()
はmap[string][]string
型のHeader
を返します。マップから直接キー"Content-Type"
を取得することで、そのキーがマップ内に存在するかどうかをhaveType
ブール値で正確に判断できます。haveType
がfalse
の場合(Content-Type
ヘッダが全く設定されていない場合): 従来のロジックと同様に、mime.TypeByExtension
やファイル内容のスニッフィングによってContent-Type
を推測し、w.Header().Set("Content-Type", ctype)
で設定します。haveType
がtrue
の場合(Content-Type
ヘッダがマップ内に存在する場合):len(ctypes) > 0
:Content-Type
ヘッダに値が設定されている場合(例:Content-Type: text/html
)。この場合、既存のctypes[0]
をctype
として使用し、スニッフィングは行いません。len(ctypes) == 0
:Content-Type
ヘッダが明示的に空のスライスとして設定されている場合(例:w.Header()["Content-Type"] = []string{}
)。この場合、else if len(ctypes) > 0
の条件は満たされないため、ctype
は初期値の空文字列のままとなり、スニッフィングも行われません。これにより、開発者がContent-Type
を意図的に空に保つことができるようになります。
src/pkg/net/http/server.go
の response.needsSniff
メソッド
needsSniff
メソッドは、レスポンスヘッダがまだ書き込まれておらず、かつ Content-Type
が設定されていない場合に true
を返します。
_, haveType := w.handlerHeader["Content-Type"]
: ここでもserveContent
と同様に、Content-Type
ヘッダがマップ内に存在するかどうかをhaveType
で直接チェックします。!haveType
:Content-Type
ヘッダがマップ内に存在しない場合にのみ、スニッフィングが必要であると判断します。これにより、明示的に空に設定されたContent-Type
ヘッダに対してはスニッフィングが行われなくなります。
テストファイルの変更
-
src/pkg/net/http/fs_test.go
:switch r.FormValue("override")
:override
パラメータの値に応じて、Content-Type
を設定するか、明示的に空にするかのロジックが追加されました。w.Header()["Content-Type"] = []string{}
:override="2"
の場合に、Content-Type
ヘッダを明示的に空のスライスとして設定しています。これが、スニッフィングを抑制する新しい挙動をテストするための重要な部分です。get := func(override string, want []string)
: テストヘルパー関数get
のシグネチャが変更され、期待されるContent-Type
が[]string
型になりました。これは、resp.Header["Content-Type"]
が[]string
を返すため、より正確な比較を行うためです。!reflect.DeepEqual(h, want)
: ヘッダの値がスライスであるため、reflect.DeepEqual
を使用してスライス全体が等しいかを比較しています。get("2", nil)
: 新しいテストケースで、override="2"
の場合にContent-Type
ヘッダがnil
(つまり、レスポンスヘッダにContent-Type
が存在しない状態)であることを期待しています。これは、w.Header()["Content-Type"] = []string{}
と設定した場合、net/http
の内部処理で最終的にヘッダが削除されるためです。
-
src/pkg/net/http/response_test.go
:TestNeedsSniff
:response
構造体のneedsSniff
メソッドの挙動をテストする新しいテスト関数が追加されました。r.handlerHeader = Header{"Content-Type": nil}
:Content-Type
ヘッダがnil
として設定された場合にneedsSniff
がfalse
を返すことを確認しています。これは、Content-Type
ヘッダがマップ内に存在しない状態と同等と見なされるためです。
これらの変更により、Goの net/http
パッケージは、開発者が Content-Type
ヘッダを明示的に空にしたいという意図を尊重し、不要な自動スニッフィングを抑制できるようになりました。
関連リンク
- Go Issue #5953: https://github.com/golang/go/issues/5953
- Go Code Review: https://golang.org/cl/14434044
参考にした情報源リンク
- HTTP Content-Type: https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Type
- MIME Sniffing: https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/X-Content-Type-Options
- Go
net/http
package documentation: https://pkg.go.dev/net/http - Go
http.Header
type: https://pkg.go.dev/net/http#Header - Go
reflect.DeepEqual
function: https://pkg.go.dev/reflect#DeepEqual