AI providers drop the cause of a transport failure from `NetworkError`
メンテナーはふだん 1 日以内に返信
評価
この 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
-
When
fetchfails,FetchHttpClientcreatesnew TransportError({ request, cause })without adescription(packages/effect/src/http/FetchHttpClient.ts:94).NodeHttpClientdoes the same (packages/platform/node/src/NodeHttpClient.ts:237,:277,:782,:805). -
NetworkErrorhasreason,request, and an optionaldescription. It has nocause(packages/effect/src/ai/AiError.ts:151). Whendescriptionis not set, itsmessagegetter usesA network error occurred.(AiError.ts:212). -
Each provider creates the
NetworkErroritself and copies onlydescription:packages/ai/anthropic/src/internal/errors.ts:113packages/ai/openai/src/internal/errors.ts:78packages/ai/openai-compat/src/internal/errors.ts:80packages/ai/openrouter/src/internal/errors.ts:114packages/ai/cloudflare/src/internal/errors.ts:87packages/ai/typesafe/src/internal/errors.ts:64
The
EncodeErrorandInvalidUrlErrorcases next to these lines have the same problem. -
AiErrorsets itscauseto itsreason(AiError.ts:1713). So the cause chain stops at theNetworkError. A loggedCause, or thetoJSONof 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-bedrockis 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.messagedoes not show the cause either (Transport error (GET http://no-such-host.invalid/)). But the cause stays on the error, so a loggedCausestill shows it. Only the AI providers lose it.
- 主要言語
- TypeScript
- スター
- 16.7k
- フォーク
- 808
- 平均マージ
- 11時間 11分
- マージ済み PR(30日)
- 840
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
Effect-TS/effect のほかの issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
@effect/ai-anthropic: text_editor_20250728 insert decodes `new_str`, but the API sends `insert_text`オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
メンテナーはふだん 1 日以内に返信
-
[email protected]: crypto/KeyFormat declarations require the global JsonWebKey, breaking Node-only type-checks対応中かも @effect-bot が今日担当しました。 オープン
Effect-TS/effect#10115 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 4/5 1〜2日 初心者へのやさしさ 55/100
メンテナーはふだん 1 日以内に返信
-
sql-pg: send several statements in one extended-query Sync (transaction-local context without extra round trips)対応中かも @effect-bot が今日担当しました。 オープン
Effect-TS/effect#10113 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
Effect-TS/effect の issue をすべて見る
似ている issue
-
難易度 1/5 1〜3時間 初心者へのやさしさ 66/100
メンテナーはふだん 1 日以内に返信
-
area:plugins area:security area:skills bug has repro
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
anthropics/claude-code#105911 ·
メンテナーはふだん 1 日以内に返信
-
documentation good first issue help wanted
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
dev-five-git/devup-ui#879 ·
メンテナーはふだん 11 日以内に返信
-
openspec-apply-change skill: when blocked, may select a ready artifact unrelated to missingArtifactsオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
Fission-AI/OpenSpec#2165 ·
メンテナーはふだん 1 日以内に返信
-
🐞 bug 🧐 unconfirmed
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
Tencent/tdesign-vue-next#7032 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信