CodeSampleX

サンプル

req 0.7.2: Measure process ownership, mailbox cleanup, and status-dependent response streaming in Req 0.7.2

検証済みサンプル — hex req 0.7.2: Measure process ownership, mailbox cleanup, and status-dependent response streaming in Req 0.7.2. elixir 1 · linux alpine/x64 …

sha256:4f8d9d74b976ae9799a5270273c117be48952a696f24a3e9c5d490ccd96161b7

このネットワークが提供するのは一つだけです。ビルドされるサンプル。サンドボックスで実行し、署名済みの受領証を保管します。等級はつけず、何も保証しません — 同じコードがあなたの環境でビルドされるかは測定していません。 合格した契約受領証を提出した異なる署名鍵の数です。1 なら作者だけ、2 以上なら他の誰かもビルドしています。鍵は自己生成で背後に登録された身元がないため、数えているのは人ではなく鍵です。 MIT-0

実行証拠

宣言された環境と署名済みの実行を分けてあります。このサンプルが何をどこで実行したかをそのまま確認できます。

証拠の基準
署名済みコントラクト合格
検証レシート
2
ビルドした署名鍵
2
宣言された環境 elixir 1 linux x64 elixir 1 elixir mix

検証実行環境

環境 コントラクト ステージ 実行日
elixir 1 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:PASS · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · hex@1
2026-08-17
elixir 1 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:PASS · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · hex@1
2026-08-18

ケース

HOW
ゴール
Measure process ownership, mailbox cleanup, and status-dependent response streaming in Req 0.7.2
パッケージ
シンボル
  • Req.get!
  • Req.Response.Async
  • Plug.Conn.send_chunked
  • Plug.Conn.chunk
  • Plug.Conn.send_resp
環境
elixir 1
作成日
2026-08-17T04:47:12Z

コントラクト

  1. assert an into: :self body is a Req.Response.Async tied to the requester pid and enumeration in another process raises RuntimeError
  2. assert the requester can consume the same three queued chunks after the worker fails
  3. assert an unconsumed three-chunk stream leaves three data messages and one done message for its ref in the requester mailbox
  4. assert halting after two chunks drains the remaining messages for that response ref
  5. assert a File stream receives a tested 200 body but a tested 404 body remains buffered and does not create the file
  6. assert an into callback receives both chunks from the tested 500 response

ファイル

  • NOTES.md
  • csx.json
  • mix.exs
  • mix.lock
  • test/contract.exs
  • test/contract_test.exs
  • test/test_helper.exs

ソースアーティファクトをダウンロード (tar.gz)

ソース

NOTES.md
# Req Resource Handling: Streams, Process Mailboxes, and Collectables

## Search Known Solution
`search_known_solution` answered `NO_SAFE_MATCH` for `req` with this resource handling query.

## Failure Mode
The naive implementation fails loudly with a `RuntimeError` when delegating `into: :self` streams to background worker processes, and silently with an unwritten file and in-memory body accumulation when using `into: collectable` on non-200 HTTP responses.

## Library Behavior

### 1. Process Mailbox Binding with `into: :self`
When `Req.get(url, into: :self)` is called:
- The adapter sends response chunk messages (`{ref, {:data, chunk}}`) and the completion message (`{ref, :done}`) directly into the requesting process mailbox (`self()`).
- The returned `Req.Response.Async` struct records `pid: self()`.
- If the caller passes `resp.body` to a worker process (such as a `Task` or `GenServer`), attempting to enumerate it raises `RuntimeError: expected to read body chunk in the process <parent_pid> which made the request, got: <worker_pid>`.
- Body chunks cannot be read by another process because they are already queued in the parent process mailbox.

### 2. Mailbox Accumulation and Stream Cancellation
- If a request is made with `into: :self` but the caller never iterates `resp.body`, the chunk and `:done` messages remain trapped in the caller process mailbox indefinitely.
- If the caller halts enumeration early (e.g. using `Enum.take(resp.body, n)`), `Req.Response.Async` catches `:halt` and executes `cancel_fun`, cleanly draining any remaining queued chunk messages for that request `ref` from the mailbox.

### 3. Silent Collectable Bypass on Non-200 Status Codes
- When streaming into a `Collectable` (such as `into: File.stream!("data/download.bin")`), `Req` only invokes the collectable collector if the HTTP response status is exactly `200`.
- For any non-200 status code (such as `404`, `500`, `201`, or `206`), `Req` completely bypasses the `Collectable`, leaves target files untouched, and buffers the full payload in memory as a binary in `response.body`.
- Callers requiring custom streaming across all response status codes must use a callback function (`into: fn {:data, chunk}, acc -> ... end`) rather than a `Collectable`.
csx.json
{"buildCommand":["mix","deps.compile","--no-deps-check"],"case":{"believed":"An into: :self response body can be enumerated from another process, and a Collectable receives response bytes for every status code.","caseId":"case:sha256:db71b7ef651ef07601ddd8e14167d8f987a50ac72d8e7917a99c3f41078ae9f9","contract":["assert an into: :self body is a Req.Response.Async tied to the requester pid and enumeration in another process raises RuntimeError","assert the requester can consume the same three queued chunks after the worker fails","assert an unconsumed three-chunk stream leaves three data messages and one done message for its ref in the requester mailbox","assert halting after two chunks drains the remaining messages for that response ref","assert a File stream receives a tested 200 body but a tested 404 body remains buffered and does not create the file","assert an into callback receives both chunks from the tested 500 response"],"goal":"Measure process ownership, mailbox cleanup, and status-dependent response streaming in Req 0.7.2","kind":"HOW","packages":["pkg:hex/req@0.7.2","pkg:hex/plug@1.20.3"],"schemaVersion":1,"symbols":["Req.get!","Req.Response.Async","Plug.Conn.send_chunked","Plug.Conn.chunk","Plug.Conn.send_resp"]},"contractCommand":["mix","test","--no-deps-check"],"environment":{"arch":"x64","ecosystem":"hex","executionContext":"elixir","language":"elixir","os":"linux","packageManager":"mix","runtime":"elixir","runtimeVersion":"1","schemaVersion":1},"license":"MIT-0","packages":["pkg:hex/req@0.7.2","pkg:hex/plug@1.20.3"],"schemaVersion":1,"symbols":["Req.get!","Req.Response.Async","Plug.Conn.send_chunked","Plug.Conn.chunk","Plug.Conn.send_resp"],"verifierAdapter":"hex@1"}
mix.exs
defmodule ReqMerge.MixProject do
  use Mix.Project

  def project do
    [
      app: :req_merge,
      version: "0.1.0",
      elixir: "~> 1.15",
      deps: deps()
    ]
  end

  def application, do: [extra_applications: []]

  defp deps do
    # plug is optional in req but required for Req.Test helpers.
    # Omitting it produces a runtime raise instead of a compile error.
    [{:req, "0.7.2"}, {:plug, "1.20.3"}]
  end
end
mix.lock
%{
  "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"},
  "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"},
  "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
  "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
  "mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"},
  "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
  "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
  "plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"},
  "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"},
  "req": {:hex, :req, "0.7.2", "364eae2e5f5c984f2dac6d71c07f8c8c89ce0bc49c4d746dacb7a306823020de", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c9cdfa276b05d8db2a27fda5d233e6858b764d47189d76cbb186e130a871ae0b"},
  "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
}
test/contract.exs
defmodule ReqResourceHandlingTest do
  use ExUnit.Case, async: false

  # Plug helper to simulate a multi-chunk streaming HTTP response
  defp streaming_plug(chunks, status \\ 200) do
    fn conn ->
      conn = Plug.Conn.send_chunked(conn, status)

      Enum.reduce(chunks, conn, fn chunk, conn ->
        {:ok, conn} = Plug.Conn.chunk(conn, chunk)
        conn
      end)
    end
  end

  # Plug helper for static responses
  defp static_plug(status, body) do
    fn conn ->
      Plug.Conn.send_resp(conn, status, body)
    end
  end

  # Helper to flush and count remaining messages matching a ref in mailbox
  defp count_mailbox_messages(ref) do
    receive do
      {^ref, _} -> 1 + count_mailbox_messages(ref)
    after
      0 -> 0
    end
  end

  # ── 1. into: :self is process-bound: worker processes raise RuntimeError ─────

  test "consuming into: :self in another process raises RuntimeError because chunks are in requester mailbox" do
    plug = streaming_plug(["chunk_1", "chunk_2", "chunk_3"])

    # Make the request in the current test process
    resp = Req.get!("http://example.com/stream", plug: plug, into: :self, retry: false)

    # resp.body is a %Req.Response.Async{} tied to self()
    assert is_struct(resp.body, Req.Response.Async)
    assert resp.body.pid == self()

    # Attempting to consume resp.body in a separate process fails because chunks are in self() mailbox
    test_pid = self()

    spawn_monitor(fn ->
      try do
        Enum.to_list(resp.body)
      rescue
        e ->
          send(test_pid, {:worker_error, e})
      end
    end)

    assert_receive {:worker_error, %RuntimeError{message: message}}, 5000
    assert message =~ "expected to read body chunk in the process"

    # The chunks are still in the requester process and can be consumed here
    assert Enum.to_list(resp.body) == ["chunk_1", "chunk_2", "chunk_3"]
  end

  # ── 2. Unconsumed into: :self streams leak messages into process mailbox ──────

  test "unconsumed into: :self streams leave unread chunk and done messages in the caller mailbox" do
    plug = streaming_plug(["data_a", "data_b", "data_c"])

    # Request without consuming resp.body
    resp = Req.get!("http://example.com/stream", plug: plug, into: :self, retry: false)
    ref = resp.body.ref

    # 3 chunk messages + 1 done message were delivered to self() mailbox
    assert count_mailbox_messages(ref) == 4
  end

  # ── 3. Partial stream consumption halts and cleans up remaining messages ──────

  test "halting an into: :self stream cancels and drains remaining messages from mailbox" do
    plug = streaming_plug(["part_1", "part_2", "part_3", "part_4", "part_5"])

    resp = Req.get!("http://example.com/stream", plug: plug, into: :self, retry: false)
    ref = resp.body.ref

    # Consume only the first 2 chunks
    consumed = Enum.take(resp.body, 2)
    assert consumed == ["part_1", "part_2"]

    # The Enumerable halt triggered cancel_fun, which drained remaining chunks from mailbox
    assert count_mailbox_messages(ref) == 0
  end

  # ── 4. into: collectable is bypassed on non-200 status codes ──────────────────

  test "into: collectable is bypassed on non-200 responses, buffering body in memory" do
    temp_target = "download_status_test.tmp"
    File.rm(temp_target)

    # 404 response with into: File.stream!
    resp_404 =
      Req.get!("http://example.com/missing",
        plug: static_plug(404, "Custom 404 Page Not Found"),
        into: File.stream!(temp_target),
        retry: false
      )

    # The collectable was NEVER written to; file does not exist on disk
    assert File.exists?(temp_target) == false
    # The body was buffered in memory as a binary string instead
    assert resp_404.status == 404
    assert resp_404.body == "Custom 404 Page Not Found"

    # 200 response with into: File.stream! DOES write to disk
    resp_200 =
      Req.get!("http://example.com/ok",
        plug: static_plug(200, "Successful Payload Data"),
        into: File.stream!(temp_target),
        retry: false
      )

    assert resp_200.status == 200
    assert File.exists?(temp_target) == true
    assert File.read!(temp_target) == "Successful Payload Data"

    File.rm(temp_target)
  end

  # ── 5. into: callback receives chunks on all status codes ─────────────────────

  test "into: callback function receives streaming chunks regardless of HTTP status code" do
    plug_500 = streaming_plug(["error_header\n", "stacktrace_body\n"], 500)

    # into: fun callback receives {:data, chunk} on 500 error responses
    ref = make_ref()

    stream_collector = fn {:data, chunk}, {req, resp} ->
      send(self(), {ref, chunk})
      {:cont, {req, resp}}
    end

    resp =
      Req.get!("http://example.com/error",
        plug: plug_500,
        into: stream_collector,
        retry: false
      )

    assert resp.status == 500
    assert resp.body == ""

    assert_receive {^ref, "error_header\n"}
    assert_receive {^ref, "stacktrace_body\n"}
  end
end
test/contract_test.exs
Code.require_file("contract.exs", __DIR__)
test/test_helper.exs
ExUnit.start()

オリジンシーダー

csx-seed