(d2-request-response)= # 第2章 HttpRequest と HttpResponse オブジェクトの解剖 第1章では `WSGIHandler.__call__` が `environ` を受け取り、`self.request_class(environ)` で `WSGIRequest` を作り、`self.get_response(request)` でレスポンスを得るところまでを追いました。 その入口で「解剖は次章へ」と送った2つのオブジェクト(ビューが受け取る `request` と、ビューが返す `response`)を、本章で開きます。 問いはこうです。 この2オブジェクトは、生の `environ`/バイト列との間で**何を肩代わり**しているのでしょうか。 なぜ `request.GET` は遅延で、`request.body` は一度しか読めず、`JsonResponse({...})` は dict 以外を拒むのでしょうか。 この章で学ぶことは次のとおりです。 - `request.GET` が `QueryDict` で遅延生成される仕組みと「複数値とイミュータブル」の理由 - `request.POST`/`request.body` の関係と「一度しか読めない」の根拠 - `MultiPartParser` がファイルアップロードをどう解析するか - `DATA_UPLOAD_MAX_MEMORY_SIZE` による本文サイズ上限(DoS 緩和) - `HttpResponse` が文字列をバイト列へ正規化する `make_bytes` - `JsonResponse(safe=True)` がなぜ dict 以外を拒むのか ```{tip} 本章も Django 5.2.15 の実物のソースを引用します(出典コメント付き)。 省略は `# ...(中略)...` で示し、その中略こそ本章で開く対象です。 ``` ## 2.1 ビューを挟む2つのオブジェクト ビューは `def view(request) -> HttpResponse` という形をしています。 左から `request` が入り、右へ `response` が出ます。 第1章で見たように、`request` は `environ` 辞書から作られ、`response` は最終的に `start_response` とバイト列イテラブルへ分解されてサーバへ渡りました。 本章はその両端、`WSGIRequest` と `HttpResponse` の内部を読みます。 ## 2.2 HttpRequest と WSGIRequest の分担 `request` の型は `WSGIRequest` で、`HttpRequest` を継承しています。 ```{code-block} python :linenos: # django/http/request.py — Django 5.2 class HttpRequest: """A basic HTTP request.""" def __init__(self): self.GET = QueryDict(mutable=True) self.POST = QueryDict(mutable=True) self.COOKIES = {} self.META = {} self.FILES = MultiValueDict() # ...(中略:path / method などの初期化)... ``` `HttpRequest` は WSGI にも ASGI にも依存しない**抽象的な基底**です。 `WSGIRequest`(第1章で読みました)はそこに「`environ` をどう読むか」という WSGI 固有の知識を足したサブクラスでした。 `self.META = environ` と辞書をそのまま参照していたのが、その固有部分です。 同じ `HttpRequest` を ASGI 側で継承するのが `ASGIRequest` で、これは第16章「Django の ASGI 対応」で読みます。 ### 2.2.1 GET:クエリ文字列の遅延解析 `request.GET` が返す `QueryDict` は、Python 標準の `dict` ではなく Django 独自の辞書サブクラスです。 HTTP のクエリ文字列は `?color=red&color=blue` のように**同じキーが複数回**現れることがあり、ふつうの辞書では後勝ちで `blue` しか残りません。 `QueryDict` は内部で値をリストとして保持し、`request.GET.getlist("color")` で `["red", "blue"]` を取り出せます。 `request.GET["color"]` のように添字でアクセスすると、最後の値が返ります。 `WSGIRequest` の `GET` は `@cached_property` です。 ```{code-block} python :linenos: # django/http/request.py — Django 5.2(WSGIRequest) @cached_property def GET(self): # The WSGI spec says 'QUERY_STRING' may be absent. raw_query_string = get_bytes_from_wsgi(self.environ, "QUERY_STRING", "") return QueryDict(raw_query_string, encoding=self._encoding) ``` `@cached_property` なので、**初回アクセス時に一度だけ**クエリ文字列を解析し、以後は結果をキャッシュします。 GET リクエストでも `request.GET` に触れなければ解析は走りません。 「触られるまで実行しない」という遅延は、第1章の起動時前払いとは逆方向の、「使う直前まで遅らせる」もう一つの省力化です。 本書では `HttpResponse` のレンダリング遅延(第5章)、QuerySet の評価遅延(第8章)、依存解決のキャッシュ(第14章)と、この遅延思想に何度も再会します。 `QueryDict` はまた**イミュータブル**(変更不可)です。 リクエスト由来のデータを誤って書き換えないための安全装置で、書き換えたい場合は `request.GET.copy()` で可変なコピーを作ります。 `QueryDict(raw_query_string, encoding=...)` と `mutable` を指定せずに生成しているため、既定でイミュータブルになります。 ### 2.2.2 POST と body:フォームと生ボディ `request.POST` の実体である `_load_post_and_files()` は、`Content-Type` を見て解析方法を切り替えます。 ```{code-block} python :linenos: # django/http/request.py — Django 5.2 def _load_post_and_files(self): """Populate self._post and self._files if the content-type is a form type""" if self.method != "POST": self._post, self._files = ( QueryDict(encoding=self._encoding), MultiValueDict(), ) return # ...(中略:本文を読み始めた後の整合性チェック)... if self.content_type == "multipart/form-data": # ...(中略:MultiPartParser によるファイルアップロードの解析)... self._post, self._files = self.parse_file_upload(self.META, data) elif self.content_type == "application/x-www-form-urlencoded": # ...(中略)... self._post = QueryDict(self.body, encoding="utf-8") self._files = MultiValueDict() else: self._post, self._files = ( QueryDict(encoding=self._encoding), MultiValueDict(), ) ``` 読み解くと、`request.POST` に中身が入る条件は二つだけだと分かります。 - `Content-Type: multipart/form-data`(ファイル付きフォーム) - `Content-Type: application/x-www-form-urlencoded`(通常の HTML フォーム) それ以外の `Content-Type`(とりわけ `application/json`)は、最後の `else` 節で**空の `QueryDict`** になります。 ```{caution} JSON ボディは `request.POST` には入りません。 `Content-Type: application/json` で送られたデータは、`json.loads(request.body)` で**自分でパース**する必要があります。 これは Django が HTML フォーム送信を POST の主用途と想定していた時代からの仕様で、`_load_post_and_files()` の分岐がその事実を端的に示しています。 (FastAPI が JSON ボディを型ヒントから自動で受け取る仕組みは第14章「実行時 DI の中核」で読み、Django との設計差を対比します。) ``` その `request.body` は、生のリクエストボディをバイト列で返すプロパティです。 ```{code-block} python :linenos: # django/http/request.py — Django 5.2 @property def body(self): if not hasattr(self, "_body"): if self._read_started: raise RawPostDataException( "You cannot access body after reading from request's data stream" ) # ...(中略:DATA_UPLOAD_MAX_MEMORY_SIZE による本文サイズの上限チェック)... try: if settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None and not did_check: # Read one byte past the limit to detect an oversize body # without loading it all into memory first. self._body = self.read(settings.DATA_UPLOAD_MAX_MEMORY_SIZE + 1) else: self._body = self.read() except OSError as e: raise UnreadablePostError(*e.args) from e finally: self._stream.close() self._stream = BytesIO(self._body) # ...(中略:読み込んだサイズの再チェック)... return self._body ``` ここに Django の設計上の工夫が三つ見えます。 - **遅延読み取り**: `if not hasattr(self, "_body"):` のとおり、`request.body` に初めて触れたときだけストリームを読みます。GET リクエストでは一度も読まれません。 - **再読み取りへの対応**: 読み終えたあと `self._stream = BytesIO(self._body)` でストリームを差し替えます。これで `request.read()` を後から呼んでも、読み込み済みのバイト列をもう一度読めます。Vol.1 で触れた「`wsgi.input` は一度しか読めない」問題への対処です。 - **二度読みの禁止**: 逆に、先にストリームを読み始めていた(`self._read_started` が真)のに後から `request.body` に触れると、`RawPostDataException` を送出して矛盾を防ぎます。「`request.body` と `request.POST` のどちらか一方しか安全に使えない」場面の根拠がこれです(`request.POST` も内部で `body` を読むため)。 ```{d2} :caption: 図2-1 request.body のストリーム状態遷移 :name: fig-d2-body-stream :align: center :width: 75% direction: down unread: "未読(初期状態)\n_stream = wsgi.input\nまだ本文を読んでいない" { style.fill: "#f1f5f9"; style.stroke: "#64748b" } bodyread: "body 読み込み済み\n_body 保持・_stream=BytesIO に差し替え\nbody / POST を安全に再読み可" { style.fill: "#dcfce7"; style.stroke: "#16a34a" } rawread: "ストリーム直読み開始済み\n_read_started = True\nrequest.read() を先に呼んだ" { style.fill: "#eff6ff"; style.stroke: "#2563eb" } err: "RawPostDataException\nread した後に body は取得不可" { style.fill: "#fee2e2"; style.stroke: "#dc2626" } unread -> bodyread: "request.body 初回アクセス" unread -> rawread: "request.read() を先に呼ぶ" rawread -> err: "後から request.body に触れる" { style.stroke: "#dc2626" } ``` ### 2.2.3 MultiPartParser:ファイルアップロードを開く `_load_post_and_files()` が `multipart/form-data` のときだけ呼ぶ `MultiPartParser`(さきほどの中略の中身)を開きましょう。 これはファイルアップロードの本体です。 ```{code-block} python :linenos: # django/http/multipartparser.py — Django 5.2(MultiPartParser._parse 抜粋) def _parse(self): # ...(中略:Content-Length が 0 のときの早期 return・ハンドラ委譲)... self._post = QueryDict(mutable=True) self._files = MultiValueDict() stream = LazyStream(ChunkIter(self._input_data, self._chunk_size)) old_field_name = None num_post_keys = 0 try: for item_type, meta_data, field_stream in Parser(stream, self._boundary): # ...(中略:直前のファイルの完了処理)... if ( item_type in FIELD_TYPES and settings.DATA_UPLOAD_MAX_NUMBER_FIELDS is not None ): # Avoid storing more than DATA_UPLOAD_MAX_NUMBER_FIELDS. num_post_keys += 1 if settings.DATA_UPLOAD_MAX_NUMBER_FIELDS + 2 < num_post_keys: raise TooManyFieldsSent( "The number of GET/POST parameters exceeded " "settings.DATA_UPLOAD_MAX_NUMBER_FIELDS." ) try: disposition = meta_data["content-disposition"][1] field_name = disposition["name"].strip() except (KeyError, IndexError, AttributeError): continue # ...(中略:item_type が FIELD なら値、FILE ならアップロードハンドラへ)... ``` ポイントは三つです。 - **boundary でパートに割る**: `Parser(stream, self._boundary)` が `multipart/form-data` の境界文字列(`Content-Type: multipart/form-data; boundary=...`)で本文をパートに分割し、各パートを `(item_type, meta_data, field_stream)` として吐き出します。 - **FIELD と FILE の振り分け**: 各パートの `Content-Disposition` から `name` を読み、`filename` があるかどうかで通常フィールド(`item_type == FIELD`)かファイル(`FILE`)かを振り分けます。前者は `self._post`、後者は `self._files` へ入ります。 - **フィールド数の上限**: `num_post_keys` を数え、`DATA_UPLOAD_MAX_NUMBER_FIELDS` を超えると `TooManyFieldsSent`。**膨大なフィールド数で辞書を膨らませる攻撃**への緩和です。 ```{d2} :caption: 図2-2 multipart 本文の boundary 分割と FIELD/FILE 振り分け :name: fig-d2-multipart :align: center :width: 70% direction: down body: "multipart/form-data 本文\nboundary で区切られた連続バイト列" { style.fill: "#f1f5f9"; style.stroke: "#64748b" } parser: "Parser(stream, boundary)\nboundary でパートに分割\n各パートの Content-Disposition を読む" { style.fill: "#eff6ff"; style.stroke: "#2563eb" } field: "パート: name=title\nfilename なし\n= 通常フィールド (FIELD)" { style.fill: "#ffffff"; style.stroke: "#9ca3af" } file: "パート: name=avatar; filename=a.png\nfilename あり\n= アップロードファイル (FILE)" { style.fill: "#ffffff"; style.stroke: "#9ca3af" } post: "self._post\nQueryDict(複数値対応)" { style.fill: "#dcfce7"; style.stroke: "#16a34a" } files: "self._files\nMultiValueDict" { style.fill: "#dcfce7"; style.stroke: "#16a34a" } body -> parser: "本文全体" parser -> field: "パート 1..n" parser -> file: "パート 1..n" field -> post: "filename なし → FIELD" file -> files: "filename あり → FILE" ``` ```{note} ファイルがメモリに載るか一時ファイルへ退避されるかは、`FILE_UPLOAD_MAX_MEMORY_SIZE` を閾値とする**アップロードハンドラ**が決めます。 小さなファイルはメモリ(`MemoryFileUploadHandler`)、大きなファイルはディスク(`TemporaryFileUploadHandler`)へ。 「大きなアップロードでメモリを使い果たさない」ための切り替えです。 ``` ### 2.2.4 本文サイズの上限:DoS 緩和 `request.body` の中略にあった `DATA_UPLOAD_MAX_MEMORY_SIZE` のチェックを開きます。 ここは**巨大な POST による DoS(メモリ枯渇)への緩和**です。 実装の妙は、`self.read(settings.DATA_UPLOAD_MAX_MEMORY_SIZE + 1)` と**上限より1 バイト多く読む**ところにあります。 全部をメモリに載せてからサイズを測るのではなく、「上限+1 バイトだけ読んで、もし読めてしまったら上限超過」と判定するのです。 上限を超えていれば(中略の再チェックで)`RequestDataTooBig` を送出します。 巨大な本文を最後まで読まずに弾けるので、DoS 緩和そのものが省メモリに行われます。 ```{important} `DATA_UPLOAD_MAX_MEMORY_SIZE`(本文全体のメモリ上限)と `FILE_UPLOAD_MAX_MEMORY_SIZE`(ファイル1個をメモリに載せる閾値)、`DATA_UPLOAD_MAX_NUMBER_FIELDS`(フィールド数の上限)は、いずれも**リクエスト由来データの暴走を抑える安全弁**です。 「外から来るデータは信用しない」という Vol.1 の原則が、Django ではこれら設定値として実装されています。 これらの上限とセキュリティ上の意味は Vol.3 で改めて扱います。 ``` ここまでの遅延生成と安全弁の関係を図にまとめます。 ```{d2} :caption: 図2-3 WSGIRequest 各属性の遅延生成と安全弁 :name: fig-d2-request-lazy :align: center :width: 100% direction: right environ: "environ 辞書\n(WSGI サーバが用意)" { style.fill: "#f1f5f9"; style.stroke: "#64748b" } req: "WSGIRequest" { style.fill: "#fffbeb"; style.stroke: "#d97706" eager: "__init__ で即時確定\nMETA / method / path / _stream" { style.fill: "#ffffff"; style.stroke: "#9ca3af" } getp: "GET\n@cached_property\nQUERY_STRING → QueryDict" { style.fill: "#dcfce7"; style.stroke: "#16a34a" } bodyp: "body\n@property\nwsgi.input を一度だけ read\n上限+1 で DoS 緩和" { style.fill: "#dcfce7"; style.stroke: "#16a34a" } postp: "POST / FILES\n_load_post_and_files()\nContent-Type で分岐" { style.fill: "#dcfce7"; style.stroke: "#16a34a" } mpp: "MultiPartParser\nboundary 分割\nFIELD / FILE 振り分け" { style.fill: "#eff6ff"; style.stroke: "#2563eb" } } environ -> req.eager: "コンストラクタ" req.eager -> req.getp: "初回アクセス時" req.eager -> req.bodyp: "初回アクセス時" req.bodyp -> req.postp: "form なら body を解析" req.postp -> req.mpp: "multipart のとき" ``` `__init__` で即時確定するのは `META`/`method`/`path` などで、`GET`・`body`・`POST`/`FILES` は初回アクセス時にだけ生成されます。`body` は上限+1 バイトの先読みで DoS を緩和し、`POST` は Content-Type に応じて `MultiPartParser` へ分岐します。 `environ` の各キーと `request` 属性の対応を一覧にしておきます。 ```{table} 表2-1 environ キーと request 属性の対応 :name: tbl-d2-request-attrs :widths: auto | environ キー | request 属性 | 生成のしかた | |---|---|---| | `REQUEST_METHOD` | `request.method` | `__init__` で即時 | | `PATH_INFO` | `request.path_info` | `__init__` で即時 | | `QUERY_STRING` | `request.GET` | 初回アクセス時(`@cached_property`) | | `CONTENT_TYPE` | `request.content_type` | `_set_content_type_params` で即時 | | `wsgi.input` | `request.body` | 初回アクセス時に一度だけ read(上限+1) | | (body を解析) | `request.POST` / `request.FILES` | 初回アクセス時(`_load_post_and_files`) | | `HTTP_COOKIE` | `request.COOKIES` | 初回アクセス時(`@cached_property`) | | `HTTP_*` 全般 | `request.headers["..."]` | `HttpHeaders` が `META` を逆変換 | | `environ` 全体 | `request.META` | `__init__` で参照を保持 | ``` ## 2.3 ビューが返す HttpResponse の構造 ここからは出口側、ビューが返す `HttpResponse` を読みます。 第1章で `__call__` が参照していた ```python status = "%d %s" % (response.status_code, response.reason_phrase) response_headers = [*response.items(), ...] ``` が、どの属性を参照していたのかの答え合わせから始めます。 ### 2.3.1 HttpResponseBase:ステータスとヘッダー レスポンスの土台は `HttpResponseBase` です。 本文を持たず、ステータスとヘッダーだけを扱います。 さきほどまで中略にしていた「Content-Type の既定値」と「status の範囲チェック」を、今回は開いて引用します。 ```{code-block} python :linenos: # django/http/response.py — Django 5.2 def __init__( self, content_type=None, status=None, reason=None, charset=None, headers=None ): self.headers = ResponseHeaders(headers) self._charset = charset if "Content-Type" not in self.headers: if content_type is None: content_type = f"text/html; charset={self.charset}" self.headers["Content-Type"] = content_type # ...(中略:headers と content_type の二重指定チェック)... self.cookies = SimpleCookie() self.closed = False if status is not None: try: self.status_code = int(status) except (ValueError, TypeError): raise TypeError("HTTP status code must be an integer.") if not 100 <= self.status_code <= 599: raise ValueError("HTTP status code must be an integer from 100 to 599.") self._reason_phrase = reason ``` 二つの既定と検証が読み取れます。 - **Content-Type の既定**: 明示がなければ `text/html; charset=...` を入れます。`HttpResponse("...")` がブラウザに HTML として表示されるのはこの一行のためです。 - **status の範囲チェック**: `100 <= status <= 599` 以外は `ValueError`。HTTP のステータスコードの範囲を、レスポンス生成の時点で守らせています。 `reason_phrase` は明示がなければ Python 標準の語句辞書から `"OK"` などを引きます(`HttpResponse(status=200)` だけで `"200 OK"` が組み上がる根拠)。 `items()` はヘッダーの `(名前, 値)` ペアを返すだけです。第1章の `[*response.items(), ...]` はこの戻り値を展開していました。 ### 2.3.2 HttpResponse:本文をバイト列へ正規化する 普段使う `HttpResponse` は、`HttpResponseBase` に**本文**を足したサブクラスです。 ここで開きたい中略は、`self.content = "..."` という代入が裏で何をしているか、すなわち `content` セッターと `make_bytes` です。 ```{code-block} python :linenos: # django/http/response.py — Django 5.2 @content.setter def content(self, value): # Consume iterators upon assignment to allow repeated iteration. if hasattr(value, "__iter__") and not isinstance( value, (bytes, memoryview, str) ): content = b"".join(self.make_bytes(chunk) for chunk in value) # ...(中略:イテレータの close)... else: content = self.make_bytes(value) # Create a list of properly encoded bytestrings to support write(). self._container = [content] self.__dict__.pop("text", None) def make_bytes(self, value): """Turn a value into a bytestring encoded in the output charset.""" if isinstance(value, (bytes, memoryview)): return bytes(value) if isinstance(value, str): return bytes(value.encode(self.charset)) # Handle non-string types. return str(value).encode(self.charset) ``` `HttpResponse("こんにちは")` と文字列を渡すと、`content` セッターが起動し、`make_bytes` が `self.charset`(既定 UTF-8)で**バイト列へ正規化**します。 PEP 3333 が要求するとおり、WSGI レスポンスの本文はバイト列でなければならないからです(docstring とコメントが PEP 3333 を明記)。 イテラブルを渡したときは各チャンクを `make_bytes` で正規化して連結し、`self._container` に**リストとして**保持します(後から `response.write()` で追記できるように)。 つまり「文字列を渡すだけでバイト列のレスポンスになる」のは、この `make_bytes` がエンコーディングを肩代わりしているからです。 Vol.1 で自作した `app` は `[b"..."]` とバイト列を手で用意していましたが、Django ではそれが `make_bytes` に集約されています。 図2-4に、`HttpResponse` を頂点とする継承ツリーと、ビューが渡す文字列が `make_bytes` を通ってバイト列になる流れを並べてまとめます。 ```{d2} :caption: 図2-4 HttpResponse の継承ツリーと本文の正規化 :name: fig-d2-response-tree :align: center :width: 100% direction: down tree: "継承ツリー (django/http/response.py)" { style.fill: "#f1f5f9"; style.stroke: "#64748b" base: "HttpResponseBase\nステータス / ヘッダーのみ\n本文を持たない" { style.fill: "#fffbeb"; style.stroke: "#d97706" } resp: "HttpResponse\nHttpResponseBase + 本文" { style.fill: "#ffffff"; style.stroke: "#9ca3af" } json: "JsonResponse\nHttpResponse の薄い派生\nsafe=True 既定" { style.fill: "#ffffff"; style.stroke: "#9ca3af" } stream: "StreamingHttpResponse\nHttpResponseBase を直接継承\n本文を逐次ストリーム" { style.fill: "#ffffff"; style.stroke: "#9ca3af" } file: "FileResponse\nStreamingHttpResponse の派生\nfile_to_stream を持つ" { style.fill: "#ffffff"; style.stroke: "#9ca3af" } base -> resp: "継承" base -> stream: "継承" resp -> json: "継承" stream -> file: "継承" } flow: "content セッターの正規化" { style.fill: "#f1f5f9"; style.stroke: "#64748b" src: "content (str)\nビューが渡す文字列" { style.fill: "#ffffff"; style.stroke: "#9ca3af" } conv: "make_bytes(value)\nself.charset でエンコード" { style.fill: "#eff6ff"; style.stroke: "#2563eb" } out: "bytes\n_container に保持\nWSGI 本文 (PEP 3333)" { style.fill: "#dcfce7"; style.stroke: "#16a34a" } src -> conv: "content セッター" conv -> out: "encode(charset)" } ``` 左の継承ツリーは `HttpResponseBase` を頂点に `HttpResponse`/`JsonResponse`/`StreamingHttpResponse`/`FileResponse` が枝分かれする関係を示し、右は `content` セッターが受け取った文字列を `make_bytes` で `self.charset` のバイト列へ正規化し、PEP 3333 が求める WSGI 本文に整える流れを示します。 ### 2.3.3 JsonResponse:safe 既定のセキュリティ REST API でおなじみの `JsonResponse` も、実体は `HttpResponse` の薄いサブクラスです。 ```{code-block} python :linenos: # django/http/response.py — Django 5.2 class JsonResponse(HttpResponse): def __init__( self, data, encoder=DjangoJSONEncoder, safe=True, json_dumps_params=None, **kwargs ): if safe and not isinstance(data, dict): raise TypeError( "In order to allow non-dict objects to be serialized set the " "safe parameter to False." ) if json_dumps_params is None: json_dumps_params = {} kwargs.setdefault("content_type", "application/json") data = json.dumps(data, cls=encoder, **json_dumps_params) super().__init__(content=data, **kwargs) ``` やっていることは、`json.dumps()` で辞書を JSON 文字列にし、`content_type="application/json"` を既定にして親 `HttpResponse.__init__` に渡すだけです。 注目は `safe=True` の既定です。 **辞書以外(とりわけトップレベルが配列の JSON)を渡すと `TypeError`** で弾きます。 これは、トップレベルが配列の JSON を返すと古いブラウザで JSON ハイジャックの脆弱性になり得た歴史への配慮です。 `JsonResponse([...], safe=False)` と明示すれば配列も返せます。この `if safe and not isinstance(data, dict):` の分岐が、その仕様の出どころです。 ```{note} 「出力の安全」をどう守るかはフレームワークで思想が分かれます。 Django は `safe=True`(トップレベル型)を守り、Starlette の `JSONResponse` は `allow_nan=False`(妥当な JSON か)を守り、FastAPI は `response_model`(宣言スキーマでフィルタ)を足します。 この3段階の対比は第15章「直列化と OpenAPI 自動生成」で完成させます。 ``` ### 2.3.4 StreamingResponse / FileResponse:第1章の file_wrapper と接続 `HttpResponse` は本文を一塊のバイト列として持ちますが、巨大な応答やファイルでは不向きです。 そのための `StreamingHttpResponse` と `FileResponse` を、第1章の `wsgi.file_wrapper` と結びつけて触れておきます。 ```{code-block} python :linenos: # django/http/response.py — Django 5.2(FileResponse 抜粋) class FileResponse(StreamingHttpResponse): block_size = 4096 def _set_streaming_content(self, value): # ...(中略:ファイルでないものは親に委譲)... self.file_to_stream = filelike = value # ...(中略:Content-Type / Content-Length / Content-Disposition の補完)... value = iter(lambda: filelike.read(self.block_size), b"") super()._set_streaming_content(value) ``` `FileResponse` は送信対象のファイルを `self.file_to_stream` に持ちます。第1章で `WSGIHandler.__call__` の末尾が `response.file_to_stream is not None` を見て `wsgi.file_wrapper` に委譲していた、その属性です。 `block_size`(既定 4096)も第1章で `wsgi.file_wrapper(response.file_to_stream, response.block_size)` に渡されていました。 ここで本章と第1章がつながります。 「アプリは `FileResponse` で何を送るかだけ決め、どう送るかはサーバの `wsgi.file_wrapper` に任せる」。リクエスト解析(入口)と応答最適化(出口)が、`file_to_stream` という一本の属性で結ばれているのです。 ## 2.4 自作 QueryDict + ミニ multipart で核を再現する `request.GET`/`request.POST` の核(「同じキーが複数回」「最後の値と全値」「イミュータブル」、そして multipart の「boundary 分割と FIELD/FILE 振り分け」)を最小コードで再現します。 ```{literalinclude} ../../../src/docs-framework/part2/d2_querydict.py :language: python :caption: d2_querydict.py:玩具 QueryDict + ミニ multipart パーサ :name: code-d2-querydict ``` 本物との対応は次のとおりです。 - **`MiniQueryDict.__getitem__` と `getlist`**: 添字は「最後の値」、`getlist` は「全値」。本物 `QueryDict` の複数値の扱いそのものです。 - **`mutable` フラグ**: 構築後は既定で書き換え不可。`request.GET` がイミュータブルなのと同じ安全装置です。 - **`parse_multipart`**: `--boundary` でパートに割り、`Content-Disposition` の `filename` の有無で**フィールドかファイルか**を振り分けます。本物 `MultiPartParser` の FIELD/FILE 振り分けの核です。 `DATA_UPLOAD_MAX_*` の上限やアップロードハンドラのメモリ/一時ファイル切替は、本書の自作では持ちません(背骨である「複数値辞書」と「boundary 振り分け」に絞っています)。 本物のソースと並べて、「何を肩代わりしているか」を確かめてください。 ## 2.5 トラブルシューティングの観点 本章で読んだソースの事実は、つまずきの切り分けに直接効きます。 ```{table} 表2-2 リクエストとレスポンスまわりの典型的なつまずき :name: tbl-d2-troubles :widths: auto | 症状 | よくある原因 | 確認の勘所(ソースの事実) | |---|---|---| | `request.POST` が空 | JSON を送っている | `_load_post_and_files` は form 系のみ解析。JSON は `json.loads(request.body)` | | `RawPostDataException` | `request.body` と `request.POST`/`read()` の併用 | ストリームは一度しか読めない。どちらか一方に統一する | | `RequestDataTooBig` | 本文が `DATA_UPLOAD_MAX_MEMORY_SIZE` 超 | 上限+1 で検出。大きな本文はアップロード機構やストリーミングへ | | `TooManyFieldsSent` | フィールド数が上限超 | `DATA_UPLOAD_MAX_NUMBER_FIELDS`。フォーム設計を見直す | | `request.GET["k"]` で最後の値しか来ない | 複数値を添字で取得 | 添字は最後の値。全値は `getlist("k")` | | `TypeError: ... set the safe parameter to False` | 配列を `JsonResponse` に渡した | `safe=True` 既定。`JsonResponse(data, safe=False)` か dict で包む | | アップロードで `text/html` になる | `Content-Type` 未指定 | `HttpResponseBase` の既定は `text/html`。明示するか専用レスポンスを使う | ``` ここまで、ビューを挟む2オブジェクトを解剖しました。 入口側では `request.GET` の遅延、複数値、イミュータブル、`request.body` の一度きり読み取り、`MultiPartParser` のファイル解析、そして `DATA_UPLOAD_MAX_*` の安全弁を開きました。 出口側では `make_bytes` のバイト列正規化、`status` の範囲チェック、`JsonResponse(safe=True)` のセキュリティ、そして `FileResponse.file_to_stream` が第1章の `wsgi.file_wrapper` とつながることを確認しました。 次章(第3章)「URL 解決の全行程」では、`get_response()` の最初の関門(リクエストされたパスから、どのビュー関数がどんな引数で呼ばれるかを Django がどう決めるか)を読み解きます。