Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

AI providers drop the cause of a transport failure from `NetworkError`

クローズ
#9,983 コメント 0 件 リアクション 0 件 担当者 1 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@effect-bot がすでに取り組んでいます。

2026年10月26日 から。

  • #9984 @wmaurer による — マージ済み

評価

この issue はまだ評価されていません。

説明

Summary

When an HTTP request fails, the caller gets an AiError with a NetworkError reason. But the NetworkError does not say why the request failed.

FetchHttpClient and NodeHttpClient store the reason in TransportError.cause. They do not set description. Every AI provider copies only description into the NetworkError, and NetworkError has no cause field. So the caller always sees TransportError: A network error occurred. It is the same message when the host does not exist, when the connection is refused, and when TLS fails.

I checked this on main at cf197c1acb, with Node 26.12.0.

Reproduction

Run this from scratchpad/:

import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
import { Effect, Layer, Redacted } from "effect"
import { LanguageModel } from "effect/ai"
import { FetchHttpClient } from "effect/http"

const program = LanguageModel.generateText({ prompt: "hello" }).pipe(
  Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
  Effect.provide(
    OpenAiClient.layer({ apiKey: Redacted.make("sk-test"), apiUrl: "http://no-such-host.invalid/v1" }).pipe(
      Layer.provide(FetchHttpClient.layer)
    )
  ),
  Effect.catchTag("AiError", (error) =>
    Effect.sync(() => {
      console.log(error.message)
      console.log("reason keys:", Object.keys(error.reason))
    }))
)

Effect.runPromise(program)

Output:

OpenAiClient.createResponse: TransportError: A network error occurred. (POST http://no-such-host.invalid/v1/responses)

Check your network connection and verify that the requested URL is accessible.
reason keys: [
  '_tag',
  'reason',
  'request',
  'description',
  '~effect/ai/AiError/Reason'
]

If you send the same request with HttpClient only, the error still has the reason:

HttpClient.get("http://no-such-host.invalid/").pipe(
  Effect.provide(FetchHttpClient.layer),
  Effect.catchTag("HttpClientError", (error) =>
    Effect.sync(() => {
      const cause = error.reason.cause as Error & { cause?: Error }
      console.log(`cause: ${cause.name}: ${cause.message}`)
      console.log(`cause.cause: ${cause.cause?.message}`)
    })),
  Effect.runPromise
)
cause: TypeError: fetch failed
cause.cause: getaddrinfo ENOTFOUND no-such-host.invalid

Cause

  1. When fetch fails, FetchHttpClient creates new TransportError({ request, cause }) without a description (packages/effect/src/http/FetchHttpClient.ts:94). NodeHttpClient does the same (packages/platform/node/src/NodeHttpClient.ts:237, :277, :782, :805).

  2. NetworkError has reason, request, and an optional description. It has no cause (packages/effect/src/ai/AiError.ts:151). When description is not set, its message getter uses A network error occurred. (AiError.ts:212).

  3. Each provider creates the NetworkError itself and copies only description:

    • packages/ai/anthropic/src/internal/errors.ts:113
    • packages/ai/openai/src/internal/errors.ts:78
    • packages/ai/openai-compat/src/internal/errors.ts:80
    • packages/ai/openrouter/src/internal/errors.ts:114
    • packages/ai/cloudflare/src/internal/errors.ts:87
    • packages/ai/typesafe/src/internal/errors.ts:64

    The EncodeError and InvalidUrlError cases next to these lines have the same problem.

  4. AiError sets its cause to its reason (AiError.ts:1713). So the cause chain stops at the NetworkError. A logged Cause, or the toJSON of the error, shows nothing below it.

There is already NetworkError.fromRequestError (AiError.ts:198), which turns a RequestError into a NetworkError. No provider uses it, and it also copies only description.

Proposed fix

Use NetworkError.fromRequestError in all six providers. When the request error has no description, fromRequestError should build one from the cause chain.

The reason is often one level below the first cause. Node's fetch fails with TypeError: fetch failed, and the useful reason, such as getaddrinfo ENOTFOUND ... or connect ECONNREFUSED ..., is in the cause of that error. If we use only the first message, the description is fetch failed. If we join the messages down the chain, with a small depth limit, the description is fetch failed: getaddrinfo ENOTFOUND no-such-host.invalid.

A refused connection to localhost needs one more step. Node tries both ::1 and 127.0.0.1. When both fail, it gives an AggregateError with an empty message. The reasons are in its errors list:

cause: TypeError: fetch failed
cause.cause: AggregateError: (empty message)
  errors: connect ECONNREFUSED ::1:11434, connect ECONNREFUSED 127.0.0.1:11434

This is what you usually get when a local model server, such as Ollama, is not running. When the message is empty, the description should use the errors list. It then reads fetch failed: connect ECONNREFUSED ::1:11434, connect ECONNREFUSED 127.0.0.1:11434.

The description must not show secrets. When a header value is not valid, fetch prints the value in its error, for example Headers.append: "Bearer sk-..." is an invalid header value. This happens when an API key contains a newline. So sensitive header values in the description should be replaced with <redacted>, in the same way as the request headers.

Alternatives

Add a cause field to NetworkError. There is an example to follow, because HttpClientErrorSchema stores its cause as Schema.optional(Schema.Defect()) (packages/effect/src/http/HttpClientError.ts:380). But the field adds public API to every AI error, and the message does not change. Most people only read the message. A description fixes the message without a new field. #9869 fixed a similar problem in the same way, with a description on the existing DecodeError.

Set description in the HTTP clients, where the TransportError is created. This would also improve HttpClientError.message. But it needs changes in FetchHttpClient, BrowserHttpClient, and four places in NodeHttpClient, in three packages. A custom HttpClient would still not set description.

Notes

  • @wmaurer/effect-ai-amazon-bedrock is a provider outside this repository. It copies the same code and has the same problem. It is tracked there as wmaurer/effect-ai-amazon-bedrock#37.
  • HttpClientError.message does not show the cause either (Transport error (GET http://no-such-host.invalid/)). But the cause stays on the error, so a logged Cause still shows it. Only the AI providers lose it.
主要言語
TypeScript
スター
16.7k
フォーク
808
平均マージ
11時間 11分
マージ済み PR(30日)
840

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

Effect-TS/effect のほかの issue

Effect-TS/effect の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

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

初心者向けの GitHub issue を短くまとめたダイジェスト。