できない.dev

HTTPリクエストにヘッダーを付けるには

リクエストヘッダーを付けて HTTP を送る基本形を各言語で示す。
クライアント全体の既定として持たせる書き方と、1 回のリクエストにだけ足す書き方の使い分けまでを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

import json
import urllib.request
 
url = "https://api.github.com/repos/python/cpython"
headers = {
    "Accept": "application/vnd.github+json",
    "User-Agent": "dekinai-dev-sample",
    "X-GitHub-Api-Version": "2022-11-28",
}
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=10) as res:
    print(res.status, res.headers.get("x-github-api-version-selected"))
    data = json.loads(res.read().decode("utf-8"))
 
print(data["full_name"])

urllib.request.Request の headers 引数に辞書を渡すと、そのままリクエストヘッダーになる。
User-Agent を明示しているのは、GitHub API が User-Agent の無いリクエストを 403 で拒否するためである。

うまくいかない時: Python で「json.decoder.JSONDecodeError: Expecting value」が解消できない

JavaScript 実行確認済み

const url = "https://api.github.com/repos/nodejs/node";
 
const res = await fetch(url, {
  headers: {
    Accept: "application/vnd.github+json",
    "User-Agent": "dekinai-dev-sample",
    "X-GitHub-Api-Version": "2022-11-28",
  },
});
if (!res.ok) {
  throw new Error(`unexpected status: ${res.status}`);
}
 
console.log(res.status, res.headers.get("x-github-api-version-selected"));
const data = await res.json();
console.log(data.full_name);

fetch の第 2 引数にある headers へオブジェクトを渡す形が最も短い。
ハイフンを含むヘッダー名はそのままだと識別子にならないため、X-GitHub-Api-Version のようなキーは引用符で囲む。

うまくいかない時: Node.js で「fetch is not defined」が解決できない(古い Node)Node.js で fetch が「self-signed certificate in certificate chain」で接続できない

TypeScript 実行確認済み

type Repo = { full_name: string };
 
const url = "https://api.github.com/repos/microsoft/TypeScript";
 
const headers = new Headers({
  Accept: "application/vnd.github+json",
  "User-Agent": "dekinai-dev-sample",
});
headers.set("X-GitHub-Api-Version", "2022-11-28");
 
const res: Response = await fetch(url, { headers });
if (!res.ok) {
  throw new Error(`unexpected status: ${res.status}`);
}
 
console.log(res.status, res.headers.get("x-github-api-version-selected"));
const data = (await res.json()) as Repo;
console.log(data.full_name);

Headers オブジェクトを組み立てておくと、set で後から足したり条件によって付け外ししたりできる。
オブジェクトリテラルを直接渡す書き方でも動くが、値が undefined になりうるヘッダーを扱うときは Headers 経由のほうが型と実体のずれが出にくい。

うまくいかない時: Node.js で「fetch is not defined」が解決できない(古い Node)

Go 静的確認

package main
 
import (
	"fmt"
	"net/http"
	"time"
)
 
func main() {
	req, err := http.NewRequest("GET", "https://api.github.com/repos/golang/go", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Accept", "application/vnd.github+json")
	req.Header.Set("User-Agent", "dekinai-dev-sample")
	req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
 
	client := &http.Client{Timeout: 10 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
 
	fmt.Println(res.StatusCode, res.Header.Get("X-GitHub-Api-Version-Selected"))
}

http.Get にはヘッダーを渡す口が無いため、http.NewRequest でリクエストを組み立ててから req.Header.Set で足す。
Set は同名ヘッダーを置き換え、Add は同名のまま追加するので、1 つだけ持たせたいヘッダーには Set を使う。

Rust 静的確認

// Cargo.toml: reqwest = { version = "0.12", features = ["blocking"] }
use reqwest::header::{ACCEPT, USER_AGENT};
 
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let res = reqwest::blocking::Client::new()
        .get("https://api.github.com/repos/rust-lang/rust")
        .header(ACCEPT, "application/vnd.github+json")
        .header(USER_AGENT, "dekinai-dev-sample")
        .header("X-GitHub-Api-Version", "2022-11-28")
        .send()?
        .error_for_status()?;
 
    println!("{}", res.status());
    Ok(())
}

reqwest では header をメソッドチェーンで重ねる。
ACCEPT のような定数が用意されているヘッダーはそちらを使うと綴り間違いを型検査で防げる。

Java 静的確認

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
 
public class Main {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.github.com/repos/openjdk/jdk"))
                .header("Accept", "application/vnd.github+json")
                .header("User-Agent", "dekinai-dev-sample")
                .header("X-GitHub-Api-Version", "2022-11-28")
                .GET()
                .build();
 
        HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(res.statusCode());
        System.out.println(res.headers().firstValue("x-github-api-version-selected").orElse("(none)"));
    }
}

HttpRequest.Builder の header を並べるとリクエスト単位でヘッダーが付く。
なお Java の HttpClient は Host や Content-Length など一部のヘッダーを制限しており、自分で設定しようとすると IllegalArgumentException になる。

C# 実行確認済み

using System;
using System.Net.Http;
using System.Threading.Tasks;
 
class Program
{
    static readonly HttpClient client = new HttpClient();
 
    static Program()
    {
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dekinai-dev-sample");
        client.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
    }
 
    static async Task Main()
    {
        using var req = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/dotnet/runtime");
        req.Headers.Add("X-GitHub-Api-Version", "2022-11-28");
 
        using var res = await client.SendAsync(req);
        res.EnsureSuccessStatusCode();
 
        Console.WriteLine((int)res.StatusCode);
        Console.WriteLine(res.Headers.TryGetValues("x-github-api-version-selected", out var v)
            ? string.Join(",", v)
            : "(none)");
    }
}

毎回同じヘッダーは HttpClient の DefaultRequestHeaders に、そのリクエストだけのヘッダーは HttpRequestMessage の Headers に置く。
User-Agent や Accept は型付きのコレクションになっているため、文字列をそのまま入れずに ParseAdd を通す。

つまずき

ヘッダーを付けたつもりで効いていない場合、まず疑うのは名前ではなく置き場所である。
Go の http.Get や Python の urlopen(url) のようにヘッダーを渡す引数が無い入口を使っていると、書いたつもりのヘッダーはどこにも乗らない。
次に多いのが上書きの方向で、クライアント既定とリクエスト個別の両方に同じヘッダーを書くと、多くのライブラリではリクエスト側が勝つ。
ヘッダー名自体は HTTP の仕様上は大文字小文字を区別しないので、Accept と accept の違いで動かなくなることはまずない。

既定に置くか、リクエストごとに置くか

User-Agent のように全リクエストで同じ値になるものはクライアントの既定に置き、ページングのカーソルや冪等キーのように 1 回ごとに変わるものはリクエスト側に置く。
この切り分けをしておくと、クライアントを使い回しながらでも値の混線が起きない。
C# の HttpClient や Go の http.Client のように 1 インスタンスを共有して使う設計では、既定に可変の値を入れると別のリクエストへ漏れるため、とくに効いてくる。

認証ヘッダーはコードに書かない

Authorization: Bearer ... のようなヘッダーはコードに直接書かず、環境変数や秘密管理から読み込む。
サンプルをそのまま貼ってトークンごとコミットする事故は珍しくないうえ、GitHub のように漏洩を検知したトークンを自動失効させるサービスもある。
ログにリクエストヘッダーをそのまま出力する処理も同じ理由で避け、出すならヘッダー名だけにとどめたい。

この記事は役立ちましたか?