2026年8月14日 金曜日
AI時短ラボ
活用· 約32

HTTP 200・79,525バイトなのに可視テキストは416文字──「取得できた」を200で数えてはいけない

ステータスコードとバイト数だけを成功条件にした取得スクリプトは、中身が空のページを「成功」として下流に流す。2026年8月14日16時06分(日本時間)にブラウザUA付きのcurlで higgsfield.ai/pricing を取得したところ、HTTP 200・79,525バイトが返ったのに、タグを剥がした可視テキストは416文字、`$`+数字の出現は0件だった。同じURLを同日16時09分にChromium(Playwright)でJS実行後に測ると、DOMは921,865バイト・可視テキスト30,106文字・`$`+数字13件で、$19から$360までの金額が出た。

執筆・編集:
目次

ページを機械で取りに行くとき、status == 200 と「バイト数が十分ある」を成功条件にすると、中身が空のページが「成功」として下流に流れる。当サイトは2026年8月14日、他社の料金ページから金額を拾おうとして実際にこれを踏んだ。返ってきたのはHTTP 200・79,525バイトで、エラーは1件も出ていない。しかしタグを剥がすと可視テキストは416文字しかなく、目的だった$+数字の出現は0件だった。直し方は、成功条件をステータスコードから外し、**「タグ除去後の可視テキスト長」と「目的の文字列の出現回数」**に置き換えることである。前者だけでも足りない。ナビゲーションとフッターだけで数百文字は出るからだ。

  1. 2026年8月14日16時06分(日本時間)、ブラウザUA付きのcurlhttps://higgsfield.ai/pricingを取得。code=200 / bytes=79525 / content-type: text/html; charset=utf-8。タグ除去後の可視テキストは416文字(HTMLエンティティを復元すると408文字)で、$+数字は0件だった【実測】
  2. 同じURLを同日16時09分にChromium(Playwright)でJS実行後に測ると、DOMは921,865バイト・同じ正規表現法で可視テキスト30,106文字・$+数字13件($19 $47 $59 $99 $129 $144 $360)。同じURLで、バイト数は11.6倍、可視テキストは72.4倍になった【実測】
  3. このレスポンスにcf-mitigatedヘッダは無かった。Cloudflare公式が案内するチャレンジ判定はステータスコードではなくcf-mitigatedヘッダとcontent-typeである。「200だから通った」も「200だからチャレンジではない」も、どちらも公式の判定条件ではない

症状:エラーが1件も出ない

これは検索して貼れるエラーメッセージが存在しない種類の事故である。出るのは正常な出力だけだ。2026年8月14日16時06分35秒(日本時間)に実行したコマンドと出力をそのまま載せる。

$ curl -sS -o hf_pricing.html \
    -w 'code=%{http_code}\nbytes=%{size_download}\ncontent_type=%{content_type}\nredirects=%{num_redirects}\n' \
    -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \
    -L 'https://higgsfield.ai/pricing'
code=200
bytes=79525
content_type=text/html; charset=utf-8
redirects=0

レスポンスヘッダの抜粋も貼る(Cookieと長大なCSPは省略した)。

HTTP/2 200
date: Fri, 14 Aug 2026 07:06:43 GMT
content-type: text/html; charset=utf-8
cf-ray: a2ae1e53ed49d552-NRT
cf-cache-status: HIT
cache-control: private, no-cache
server: cloudflare
x-hf-worker: tanstack
x-clerk-auth-status: signed-out
x-edge-cache: hit

200が返り、79,525バイトが落ちてきて、server: cloudflareが付いている。ここまでは何も異常が無い。異常が見えるのはタグを剥がしてからである。79,525バイトから取れた可視テキストは、これが全部だった。

Pricing plans — Higgsfield AI Video & Image Generator Higgsfield Explore Image Video Audio Edit Layers Cinema Studio New Marketing Studio New Viral Presets MCP & CLI New Supercomputer Academy New Community Contests New Plugins Canvas Originals Pricing 30% OFF Enterprise Login Sign up © 2026 Higgsfield, Inc. All rights reserved. English Help center Cookie Notice Terms Privacy Home Community Library Profile

グローバルナビゲーションとフッターのリンクラベルだけである。料金ページなのにプラン名も金額も無い。\$\s?\d$の後ろに数字)の出現回数は、この可視テキストで0件、タグを剥がす前の生HTML 79,525バイト全体でも0件だった。スクリプトタグの中も含めて、$表記の金額はHTMLのどこにも無い。 ただし$を伴わない数値(JSON中の19のような値)として残っている可能性までは調べていない。

注意すべきは30% OFFが出ている点である。数字も記号も「ある」。無いのは目的の数字だけだ。 「何か文字が取れたか」を成功条件にすると、この判定は通ってしまう。

原因1:200は「欲しいデータが入っている」とは言っていない

RFC 9110(HTTP Semantics)の15.3.1「200 OK」はこう定義している。

The 200 (OK) status code indicates that the request has succeeded. The content sent in a 200 response depends on the request method.

(200 (OK) ステータスコードは、リクエストが成功したことを示す。200レスポンスで送られるコンテンツはリクエストメソッドに依存する)

同じ節の表は、GETに対する200の意味を「the target resource(ターゲットリソース)」の表現である、と書く。さらに次の一文がある。

Aside from responses to CONNECT, a 200 response is expected to contain message content unless the message framing explicitly indicates that the content has zero length.

(CONNECTへの応答を除き、200レスポンスはメッセージコンテンツを含むことが期待される。ただしメッセージフレーミングがコンテンツ長ゼロを明示している場合を除く)

MDNの「200 OK」(最終更新2026年6月22日)も同じ範囲しか言っていない。

The HTTP 200 OK successful response status code indicates that a request has succeeded. A 200 OK response is cacheable by default.

GET: A resource was retrieved by the server and included in the response body.

(HTTPの200 OK成功レスポンスのステータスコードは、リクエストが成功したことを示す。200 OKレスポンスは既定でキャッシュ可能である/GET:リソースがサーバーで取得され、レスポンスボディに含められた)

規格が保証しているのは「URLに対応する表現が返った」ことまでで、「その表現が自分の欲しい情報を含む」ことではない。 空のシェルHTMLも、チャレンジ用の待機ページも、ログイン誘導ページも、そのURLの正当な「表現」でありうる。200はそれらを区別しない。区別するのは取得側の責任である。

原因2:Cloudflare公式の判定材料もステータスコードではない

Cloudflareのチャレンジ関連ドキュメントを2026年8月14日に読み直した。まず「Detect a Challenge Page response」(最終更新2026年5月5日)である。

When a request encounters a Cloudflare Challenge Page instead of the originally anticipated response, the Challenge Page response (regardless of the Challenge Page type) will have the cf-mitigated header present and set to challenge. This header can be leveraged to detect if a response was challenged when making fetch/XHR requests.

(リクエストが本来期待していたレスポンスの代わりにCloudflareのチャレンジページに遭遇した場合、そのチャレンジページのレスポンスは、チャレンジページの種類にかかわらずcf-mitigatedヘッダを持ち、値はchallengeに設定される。このヘッダは、fetch/XHRリクエストでレスポンスがチャレンジされたかどうかを検出するのに利用できる)

同ページは、Noteの囲みと本文で次のようにも書いている(英文はどちらも原文のままで、Noteの見出し語は含めていない)。

Regardless of the requested resource-type, the content-type of a challenge will be text/html.

For the cf-mitigated header, challenge is the only valid value. The header is set for all Challenge Page types.

(要求されたリソースタイプにかかわらず、チャレンジのcontent-typetext/htmlになる/cf-mitigatedヘッダについて、challengeが唯一の有効な値である。このヘッダはすべてのチャレンジページタイプで設定される)

このページはチャレンジ応答のステータスコードを一言も書いていない。 公式が「信頼できる判定方法(a reliable way to identify whether a response is a Challenge or not)」として提示しているのはcf-mitigatedヘッダであり、ステータスコードではない。JSON APIを叩いたつもりでcontent-type: text/htmlが返ってきたら、それ自体が疑うべき信号になる。

チャレンジページが何をするかは「Interstitial Challenge Pages」(最終更新2026年7月6日)に書かれている。

Challenge Pages interrupt the request flow by returning a full HTML page for the user's browser to render and solve. This mechanism fails when the browser expects a non-HTML response, such as an AJAX or XHR (fetch) request.

(チャレンジページは、ユーザーのブラウザがレンダリングして解くための完全なHTMLページを返すことでリクエストの流れを中断する。この仕組みは、ブラウザがAJAXやXHR(fetch)リクエストのようにHTML以外のレスポンスを期待している場合には破綻する)

同ページは「非対話型チャレンジ」について「The visitor must wait until their browser finishes processing the JavaScript, which typically takes less than five seconds.(訪問者はブラウザがJavaScriptの処理を終えるまで待たなければならない。通常5秒未満である)」とも書いている。JavaScriptを実行しない取得クライアントは、この「5秒未満の処理」を永久に終えない。 待機用のHTMLを受け取り、それをコンテンツとして下流に渡す。

今回のレスポンスはチャレンジではなかった

正直に書くと、当サイトが今回踏んだのはチャレンジページではない。 保存したレスポンスヘッダをgrep -ci 'cf-mitigated'で数えた結果は0件だった。つまりCloudflareの判定基準では、これは「チャレンジされたレスポンス」ではない。x-hf-worker: tanstackという独自ヘッダが付いていることから、サイト側のフレームワークが返したHTMLだと読める(ヘッダ名からの推測であり、サイト構成を確認したわけではない)。中身が無いのはブロックされたからではなく、本文がクライアント側のJavaScriptで後から入る作りだからだと考えている。根拠は後述のChromiumでの再測定である。

ただし、このHTMLには/cdn-cgi/challenge-platform/scripts/jsd/main.jsというスクリプト参照が1件含まれていた。Cloudflareの「How Challenges work」(最終更新2026年7月6日)はJavaScript Detectionsをこう説明している。

JavaScript Detections is an optional feature within Bot Management. When enabled, Cloudflare injects a JavaScript snippet into HTML responses to gather client-side signals. Unlike Challenge Pages, JavaScript Detections runs on every HTML request without pausing or interrupting the visitor.

(JavaScript DetectionsはBot Management内のオプション機能である。有効にすると、CloudflareはHTMLレスポンスにJavaScriptスニペットを注入してクライアント側のシグナルを集める。チャレンジページと違い、JavaScript Detectionsは訪問者を止めたり中断したりせずに、すべてのHTMLリクエストで実行される)

当サイトが確認したのは「そのURL文字列がHTMLに1件含まれていた」という事実までである。それがJavaScript Detectionsの注入分だと断定はしていない。重要なのは、「チャレンジではなかった」場合でも結果は同じ空ページだったという点だ。 原因が2種類あって症状が1つなら、症状の側で検知するしかない。

原因3:バイト数はJSの量であって情報量ではない

79,525バイトのうち、可視テキストは416文字である。比率にして0.52%。同じ日に同じ方法で測った対照を1つ置く。

取得対象(2026年8月14日・同一スクリプト) バイト数 タグ除去後の可視文字数 可視比率
higgsfield.ai/pricing 79,525 416 0.52%
developers.cloudflare.com/cloudflare-challenges/(サーバー側で本文を返す文書ページ) 104,591 3,275 3.13%

バイト数はむしろ後者の方が大きい。バイト数の大小では、この2つを区別できない。 区別できるのは可視テキスト長の方である。

同じURLをJS実行後に測るとどうなるかも確かめた。2026年8月14日16時09分(日本時間)、Playwright経由のChromiumでhttps://higgsfield.ai/pricingを開き、ページ内で測定した結果である。

測定方法 バイト数 可視文字数 $+数字の出現
curl(JS実行なし・タグ除去の正規表現) 79,525 416 0件
Chromium(JS実行後・同じ正規表現outerHTMLに適用) 921,865 30,106 13件
Chromium(JS実行後・document.body.innerText 921,731※ 14,333 13件

innerTextの測定は13秒前に取得しており、そのときのDOMは921,731バイトだった。

検出できた金額の内訳は$19 $47 $59 $99 $129 $144 $360の7種・13箇所である。JSを実行するかどうかだけで、同じURLの可視テキストが416文字と30,106文字に分かれた。 取得側がどちらの数字を「取得成功」と呼ぶかは、取得側の設計の問題であって、サーバーの応答の問題ではない。

対処:成功条件を書き換える

当サイトが今回の実測に使ったスクリプトをそのまま載せる。可視テキストの測り方はここで定義したものを指す。

import re, html as h

raw = open('page.html', encoding='utf-8', errors='replace').read()

b = re.sub(r'(?is)<script[^>]*>.*?</script>', ' ', raw)
b = re.sub(r'(?is)<style[^>]*>.*?</style>',  ' ', b)
b = re.sub(r'(?is)<noscript[^>]*>.*?</noscript>', ' ', b)
b = re.sub(r'(?s)<!--.*?-->', ' ', b)
text = re.sub(r'(?s)<[^>]+>', ' ', b)
text = re.sub(r'\s+', ' ', text).strip()      # ← この時点で416文字
# text = h.unescape(text)                     # ← エンティティを復元すると408文字

print(len(text), len(re.findall(r'\$\s?\d', text)))

そのうえで、取得の成功条件を次の順で置き換える。

  1. 目的の文字列の出現回数を成功条件の本体にする。 料金を拾うなら$+数字、日付を拾うなら日付パターン、件数を拾うならその単位。0件なら、200でも失敗として扱う。 今回の30% OFFのように、目的外の数字は普通に取れてしまう
  2. タグ除去後の可視テキスト長を第2の条件にする。 ナビゲーションとフッターだけで数百文字は出るので、「0文字でないこと」では機能しない。当サイトの実測では、空のシェルが416文字、本文があるページが3,275文字だった
  3. cf-mitigatedヘッダを見る。 Cloudflare公式が「チャレンジページの種類にかかわらず」設定されると書いている値であり、値はchallengeのみ
  4. content-typeを検査する。 JSONを期待した先からtext/htmlが返ったら、その時点で異常として扱う。公式は「要求されたリソースタイプにかかわらず、チャレンジのcontent-typetext/htmlになる」と書いている
  5. 失敗したURLを黙って捨てない。 「0件だった」と「取得に失敗した」を別のフィールドに記録する。両者を1つのカウンタにまとめると、下流では区別できなくなる
  6. どうしても中身が要るなら、JSを実行する取得系に切り替える。 今回は同じURLで、可視テキストが416文字から30,106文字になった

閾値の具体的な数値は挙げない。当サイトが今回測ったのは2ページ(416文字と3,275文字)だけで、この2点から一般的な境界を引くことはできない。決められるのは「目的の文字列が0件なら失敗」という条件の方である。 これは対象ごとに定義でき、閾値の当てずっぽうを含まない。

観測条件(2026年8月14日・当サイト1件)

時刻(日本時間) 実行内容 結果
2026-08-14 16:06:35 ブラウザUA付きcurl -Lhttps://higgsfield.ai/pricingを取得 code=200 / bytes=79525 / リダイレクト0回
2026-08-14 16:06:43 同URLのレスポンスヘッダを保存 server: cloudflare / cf-ray: a2ae1e53ed49d552-NRT / cf-cache-status: HIT / cf-mitigated無し
2026-08-14 16:06 台 保存HTMLをタグ除去して測定 可視416文字(エンティティ復元後408文字)/$+数字は可視・生HTMLとも0件
2026-08-14 16:09:36 Playwright経由のChromiumで同URLを開きinnerTextを測定 DOM 921,731バイト/可視14,333文字/$+数字13件
2026-08-14 16:09:49 同じページでouterHTMLに上記の正規表現を適用 DOM 921,865バイト/可視30,106文字/$+数字13件

環境はmacOS(Darwin 24.1.0)、curlはコマンドライン、ブラウザ側はPlaywright経由のChromium。両方とも未ログインである。 ただし根拠は別々で、curl側はレスポンスヘッダのx-clerk-auth-status: signed-out(この値はcurlのリクエストに対するもので、ブラウザ側の状態を示すものではない)、ブラウザ側は16時09分30秒に保存したページのアクセシビリティ・スナップショットにLoginSign upのボタンが出ていることである。本文に載せた取得はそれぞれ1回で、時間を空けた再現テストは行っていない(公開前の検証で測り直した結果は後述する)。

当サイトがこの現象を最初に記録したのは同じ2026年8月14日の別作業で、そのときの記録は「200・79,316バイト・可視テキスト408文字・ドル金額0件」だった。本記事の再取得では79,525バイトに増えている。バイト数が違うのはページが更新されているためで、同じ数字が出ることを期待してはいけない。

正直な但し書き

416文字と408文字の差は測り方の差であり、ページの差ではない。 差の8文字は、可視テキストに残った&amp;が2箇所(ページタイトル部分のVideo &amp; Image Generatorと、ナビゲーションのMCP &amp; CLI)あることで説明が付く。1箇所あたり4文字、2箇所で8文字である。HTMLエンティティを復元すれば408、しなければ416になる。同じページを測っても、方法を書かずに文字数だけ持ち出せば数字は食い違う。 最初の記録が408だった理由がエンティティ復元だったのかは、記録に残っていないので確定できない。

「JSレンダ前のシェルだから空だった」という説明は、当サイトの観測と整合するというところまでである。 確認したのは、(a) 生HTML 79,525バイトに$+数字が0件、(b) 同じURLをChromiumでJS実行後に測ると13件、(c) cf-mitigatedヘッダが無い、の3点である。サイト側がどのタイミングでどのAPIから金額を取得しているかは追っていない。

Cloudflareのチャレンジ応答が何番のステータスコードを返すかは、本記事では確認していない。 参照した3ページ(Challenges概要/Detect a Challenge Page response/Interstitial Challenge Pages)はいずれもステータスコードに言及していない。当サイト自身がチャレンジページを引いた実測も持っていない。したがって本記事は「チャレンジページは200で返る」とは書いていない。書いているのは「公式が案内する判定はステータスコードではなくcf-mitigatedヘッダとcontent-typeである」という点だけである。

タグ除去の正規表現は厳密なHTMLパーサではない。 属性値に>が含まれる場合や壊れたマークアップでは結果が変わりうる。実際、Chromium側でouterHTMLに同じ正規表現を当てた30,106文字と、ブラウザ自身のinnerTextによる14,333文字には2倍以上の開きがある。「可視テキスト」の値は測り方に依存するので、閾値を置くなら測り方を固定したうえで、自分の対象で較正する必要がある。

Chromium側のバイト数とinnerTextの文字数は、1回のロードの値である。 公開前の検証として、同じ2026年8月14日の16時38分〜16時40分(日本時間)に同じ手順で測り直した。outerHTMLに同じ正規表現を当てた可視テキストは30,106文字、$+数字は13件、金額の内訳も$19 $47 $59 $99 $129 $144 $360の7種で本文と一致した。一方でDOMのバイト数は920,524〜920,658、innerTextは14,379文字で、本文の921,731/921,865バイト・14,333文字とは一致していない。さらに、その直前の1回のロードではDOM 997,847バイト・可視テキスト30,406文字・innerText 14,647文字となり、$+数字は13件のままだが内訳が$9 $19 $23 $29 $47 $59 $72 $79 $99 $144 $240の11種に変わった。DOMのバイト数・innerTextの文字数・金額の内訳は、同じURLでもロードごとに動く。固定値として引用してはいけない。 本記事が主張しているのは特定の数値ではなく、「JSを実行しない取得では可視テキストが数百文字・目的の文字列0件で、JSを実行すると桁が変わる」という差の方である。

引用したCloudflareのドキュメントは英語版である。 本記事の訳は当サイトで付けたもので、訳の責任は当サイトにある。また、チャレンジ関連の解説でよく引かれるhttps://developers.cloudflare.com/waf/reference/cloudflare-challenges/というURLは、2026年8月14日時点でhttps://developers.cloudflare.com/cloudflare-challenges/へ301リダイレクトされる。出典には移動後のURLを記載した。

対象ページの運営会社と当サイトには取引関係がある。 本記事はそのページの内容や料金が適切かどうかについては何も述べておらず、機械取得の挙動だけを扱っている。

出典

シェア: ポスト はてブ

出典・参照資料

AIニュースの解説を動画でも

YouTubeでは注目ニュースの背景を解説し、Xでは新着記事をお知らせしています。

コメント

まだコメントはありません。最初のコメントを書いてみませんか?

AIについて聞きたいことはありますか?

質問箱で無料で受け付けています。回答は公開され、他の方の参考にもなります。

質問箱を見る →

新しい記事をメールで受け取る

AIの新しい発表を、出典付きで整理して届けます。

関連記事

AI時短ラボ
活用08.14読了42

IndexNowにGoogleは参加していない──418URL送ってもGoogleには渡らない仕組みと、鍵が無効なときに返る403 UserForbiddedToAccessSite

出典 ─ IndexNow - Home(indexn
AI時短ラボ
活用08.14読了26

出典URLの死活チェックで404だけ見てはいけない──1,517件を全数検査したら、HEADに404を返す公式ドキュメントと403の一次資料が出てきた

出典 ─ RFC 9110: HTTP Semanti
AI時短ラボ
活用08.14読了21

curlに-Lを付けずに308を叩くと15バイトの「Redirecting...」が返る──sitemapが0件・サイトが壊れたと誤診する罠

出典 ─ 308 Permanent Redirect(HTTP response status codes)
AI時短ラボ
活用08.14読了34

Search Console APIのsites.addはエラーを返さないのにデータは1件も読めない──permissionLevelがsiteUnverifiedUserで止まる二段構え

出典 ─ Sites: add | Search Co
AI時短ラボ
活用08.14読了31

URL Inspection APIのレスポンスを機械判定すると壊れる3点──coverageStateは表示文字列、ROBOTS_TXT_STATE_UNSPECIFIEDはrobots.txtの問題ではない

出典 ─ Method: index.inspect
AI時短ラボ
活用08.14読了36

www化の308を入れてもSearch Consoleの旧プロパティは集計を続ける──URLプレフィックス型で5週間データを取り逃した記録

出典 ─ Add a website or platf
AI時短ラボ
活用08.14読了24

macOSのawkはIGNORECASE=1をエラーも出さず無視する──Linux前提のワンライナーが「検出0件」で静かに通る

出典 ─ GAWK: Effective AWK Pr
AI時短ラボ
活用08.14読了28

Vercel CLIはgitではなく作業ツリーを直接デプロイする──git statusが本番の中身を表していない状態の見分け方

出典 ─ Exclude Files from Dep