har

HAR ツール

HARはブラウザが観測したHTTP通信をそのままJSONに落としたファイル。log.entries[]の構造、blockedからreceiveまで7つのタイミング指標、ウォーターフォールの読み方、そして共有前に消すべきCookieやトークンの扱いまでを整理します。

2 ツール

§01 分野ガイド

HARは通信の記録であって、パケットキャプチャではない

HAR(HTTP Archive)は、ブラウザが観測したネットワーク通信を1つのJSONに落とした ファイルです。本体はlog.entries[]で、リクエスト/レスポンス1組が1要素になり、 メソッド・URL・ヘッダー・Cookie・サイズ・フェーズ別のタイミング、場合によっては 本文までを持ちます。仕様はHAR 1.2。W3Cのドラフトのまま標準化されませんでしたが、 Chrome・Firefox・Safari・Charles・Fiddlerが互換な方言を出力するのでツール間を 行き来できます。

押さえるべき前提は、このファイルがブラウザのネットワークスタック自身によって 事後的に書かれる点です。記録されるのはワイヤー上のバイト列ではなくスタックの 要約で、TLSハンドシェイクの記録もHTTP/2のフレームもDNSパケットも入りません。DevToolsを開く前に飛んだリクエストは残らず、Preserve logを有効に していなければ前のナビゲーションも消えます。逆にHTTPキャッシュやService Worker が返した応答は、ネットワークに出ていないのにエントリとして現れます。同じ不具合の HARが2つで食い違うのは、たいていこれが理由です。

ファイルの構造

log
├─ version "1.2" · creator{name,version} · browser
├─ pages[]     id, title, startedDateTime, pageTimings{onContentLoad,onLoad}
└─ entries[]   pageref, startedDateTime, time, connection, serverIPAddress
   ├─ request  method, url, httpVersion, headers[], cookies[],
   │           queryString[], postData{mimeType,text,params}
   ├─ response status, statusText, redirectURL, headers[], cookies[],
   │           content{size,compression,mimeType,text,encoding}
   ├─ cache    beforeRequest, afterRequest
   └─ timings  blocked, dns, connect, ssl, send, wait, receive

request.headers[]request.cookies[]は同じCookieの別表現なので、ヘッダーだけ を消す処理ではCookieが残ります。_で始まるキー(_initiator_priority_resourceType_webSocketMessages)はベンダー拡張です。Chromeでは手掛かりに なりますが、他の書き出し元では欠けていたり中身が違っていたりします。 数値フィールドは-1を取り得ますが、これは「値なし」であって0ではありません。

7つのタイミング指標

単位はミリ秒。発生順に並びます。

指標測っているもの値が大きいとき-1になる条件
blocked送出前の待ち行列HTTP/1.1の1オリジン約6接続の上限、プロキシ交渉待たされていない
dnsホスト名の解決リゾルバのキャッシュミス、長いCNAME連鎖接続を再利用
connectTCPハンドシェイク(sslを含む)オリジンが遠い、RTTが大きい、keep-aliveなし接続を再利用
sslTLSネゴシエーション(connectに内包)証明書チェーンが長い、セッション再開なし平文HTTPまたは再利用
sendリクエストの送出アップロード本文が大きい
wait送信完了後のTTFBバックエンドの処理時間+RTT 1往復
receive本文の受信非圧縮または巨大なペイロード

entry.timeは実質blocked + dns + connect + send + wait + receiveの合計です。 sslconnectに含まれるため、7つを単純に足すとハンドシェイクを二重計上します。 これがHAR分析で最も多い計算ミスです。

ウォーターフォールの読み方

横位置はentry.startedDateTimeから所属するlog.pages[].startedDateTimeを引いた 値、バーの幅はentry.timeをフェーズで割ったものです。entry.connectionが同じ エントリは、同一ソケットを再利用しています。

数字より形を見ます。前が終わってから次が始まる階段状の並びは依存の連鎖 (HTML → JS → API → 画像)で、サーバーを速くしても縮みません。発見を前倒しする しかありません。下に行くほどblockedが伸びる密集帯はクライアント側の待ち行列で、 HTTP/2で個々の応答が速くならないままウォーターフォールが平らになるのはこの ためです。

ツールの使い分け

まずHARビューアーに貼り付けます(SRE向けの一覧 にも入っています)。log.entries[]Method / Status / Type / Size / Time / URL の表に展開し、N requests · X KB · Y ms totalの要約を出します。Statusセルは 4xx/5xxが赤、3xxがオレンジで色付くので、外れ値がすぐ見つかります。読むのは メタデータだけで、本文は描画しません。

ビューアーが出すのはエントリ単位の合計値で、フェーズの内訳ではありません。内訳が 必要ならJSONPathファインダーで生のJSONを直接叩きます。 $..timingsで全件、$.log.entries[?(@.time>1000)].request.urlで遅いものだけ。 そもそもパースできない(ダウンロードが途中で切れた、貼り付けで折り返された)場合は JSONフォーマッターにかけると、パーサーのエラー位置が分かります。

Statusの解釈はHTTPステータスコードで詰めます。番号から名称・ クラス・1行の意味を引ける表で、コードでも語句でも絞り込めるため、400422の 切り分けはここで即座に付きます。扱うのは意味までで、リダイレクトの挙動までは 含みません(301の行は「恒久的に移動した」としか書いていません)。チェーンを読む ときはプロトコル側の事実で補います——301302はクライアントがPOSTGETに 書き換えてボディを捨てることを許し、307308は許しません。比較表は HTTP関連ツールのリダイレクト選択表にあり、ヘッダー側も同じハブの 担当です。キャプチャがなくURLだけある場合は HTTPヘッダーapi.sitekits.dev経由でサーバー側 から取得し、ステータス・リダイレクト回数・全レスポンスヘッダーを返します(本文は 取得せず、URLも保存しません)。401のBearerトークンは JWTデコーダーでローカルにデコードしてexp切れを確認でき、 再現はREST APIテスターがブラウザから対象へ直接送るので CORSも本番と同じ挙動になります。

よくある落とし穴

生のHARは認証情報そのもの

セッションCookie、Authorization: Bearer …x-api-key、署名付きURL、ログインの POST本文、そのセッションで読めたレスポンス本文が全部入っています。 HARサニタイザーはブラウザ内で完結し(アップロードしません)、 該当ヘッダーとtoken|key|secret|password|passwd|pwd|auth|session|sig|signatureに一致するパラメータ、 postData.textcontent.text[REDACTED]に置き換え、件数を表示します。 ブラウザ側に「サニタイズ済み」の書き出しがあっても、何を消すかはバージョン次第 なので通しておきます。

パターンマッチは保証ではない

ヘッダー名は固定リスト、パラメータ名は正規表現での照合なので、変則的なものは 残ります。URLのパスに埋まった秘密(/v1/reset/9f3c…)、構造化された cookies[]配列、serverIPAddress(オリジンの実IP)、内部ホスト名などです。出力には 必ず目を通し、怪しいURLはURLパーサーでクエリを展開して確認し、 渡す前にauthorizationset-cookieをgrepします。同じ発想は プライバシー関連ツールセキュリティ向けの一覧にも通じます。

content.sizebodySizeは別物

content.sizeはデコード後の本文長、bodySizeは実際に受け取ったバイト数で、 削減分はcontent.compressionに入ります。キャッシュから返った応答は bodySize: 0です。content.sizeを足したKB合計は、圧縮されたサイトの実際の 転送量を過大に見せます。

FAQ
HARの合計時間が実際のページ表示時間と合わないのはなぜですか?
リクエストが並行しているためです。log.entries[]のentry.timeを足し上げると、2本以上が同時に飛んでいた区間を重複して数えるので、合計は実際の経過時間の数倍になることが普通です。壁時計の時間が知りたい場合はlog.pages[].pageTimings.onLoadを読むか、最も早いstartedDateTimeから最も遅いstartedDateTime+timeまでの区間を取ります。
HARのwaitが長いときは何を疑えばよいですか?
waitはリクエストの最後のバイトを送り終えた後のTTFBなので、サーバー側の処理時間にネットワークの1往復ぶんが乗った値です。waitが太くreceiveが細い形はバックエンド側、つまり遅いクエリ・コールドキャッシュ・リージョン跨ぎの通信を疑います。逆にwaitが細くreceiveが太い場合は、単純にレスポンスが大きいか回線が遅いだけです。
HARのCookieヘッダーを消したのにセッションCookieが残っているのはなぜですか?
HARはCookieを二重に持っているためです。生のCookie行はrequest.headers[]にありますが、同じ値がrequest.cookies[]とresponse.cookies[]にも名前と値のオブジェクトとして入っており、ヘッダーだけを走査する処理では後者がそのまま残ります。当サイトのHARファイルサニタイザーも同様で、ヘッダー値・クエリとPOSTのパラメータ・両方の本文は置換しますが、cookies[]配列には手を付けません。添付する前にファイル内をcookiesで検索し、ついでにserverIPAddressやURLのパスに埋まった秘密も確認してください。
HARのエントリでdnsやconnectが-1になっているのはなぜですか?
そのリクエストではそのフェーズが発生しなかったからです。HAR 1.2の-1は「該当しない/取得できない」を意味し、既存のkeep-alive接続に乗ったリクエストではDNS解決もTCPハンドシェイクもTLSネゴシエーションも不要なのでこの値になります。表示上0として扱うのは無害ですが、実測値として平均に含めると数字が壊れます。
HARにリクエストやレスポンスの本文が入っていないのはなぜですか?
postData.textとcontent.textは任意フィールドで、DevToolsは大きな本文やバイナリを省くことがよくあります。本文がある場合もプレーンテキストではなくBase64のことがあり、そのときはcontent.encodingがbase64になります。本文が無いことは「レスポンスが空だった」ではなく「書き出し側が記録しなかった」を意味します。