From cec5c4fdfc474b20874dc3804594b6d8c1183aa1 Mon Sep 17 00:00:00 2001 From: "LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Thu, 7 May 2026 17:27:45 +0200 Subject: [PATCH] fix(http): make handler-error status visible in access log + transcription errors (#9707) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(http): log accurate status code when handler returns error The custom xlog access-log middleware in API() reads res.Status *before* Echo's central HTTPErrorHandler runs, so when a handler returns an error without writing a response (e.g. TranscriptEndpoint's `return err` on backend failure) the status field stays at its default 200. The logged line then claims status=200 while the client receives 500 — silently hiding every 500/503/etc. that bubbles up through Echo's error handler. Mirror echo.DefaultHTTPErrorHandler's status derivation when err != nil and the response hasn't been committed: default to 500, upgrade to *echo.HTTPError.Code if applicable. The logged status now matches what the client actually sees, so failed transcription requests stop appearing as 200 in the access log. Signed-off-by: Ettore Di Giacinto Assisted-by: Claude:claude-opus-4-7 [Claude Code] * fix(transcription): log underlying error before returning 500 to client ModelTranscriptionWithOptions surfaces real failures — gRPC errors from a remote node, model load problems, ffmpeg conversion crashes — but TranscriptEndpoint just did `return err`, so Echo turned it into a 500 with a generic body and the original error was lost. Operators chasing transcription failures across distributed mode were left with "upstream returned 500" on the client and zero context anywhere in the frontend's logs. Add an xlog.Error before returning, recording model name, the staged audio path, and the underlying error. Combined with the access-log status fix, a failing transcription now leaves an audit trail (real status code in the access line, real cause in an Error line) instead of vanishing. Signed-off-by: Ettore Di Giacinto Assisted-by: Claude:claude-opus-4-7 [Claude Code] --------- Signed-off-by: Ettore Di Giacinto Co-authored-by: Ettore Di Giacinto --- core/http/app.go | 21 ++++++++++++++++++--- core/http/endpoints/openai/transcription.go | 9 +++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/core/http/app.go b/core/http/app.go index e18690def..bfa47c584 100644 --- a/core/http/app.go +++ b/core/http/app.go @@ -167,6 +167,21 @@ func API(application *application.Application) (*echo.Echo, error) { res := c.Response() err := next(c) + // Echo's central HTTPErrorHandler runs *after* this middleware + // returns, so res.Status still reads the default 200 here when a + // handler returned an error without writing a response. Mirror + // echo.DefaultHTTPErrorHandler's status derivation so the access + // log reflects the status the client actually receives — without + // this, every silent handler error logs as 200. + status := res.Status + if err != nil && !res.Committed { + status = http.StatusInternalServerError + var he *echo.HTTPError + if errors.As(err, &he) { + status = he.Code + } + } + // Fix for #7989: Reduce log verbosity of Web UI polling, resources API, and health checks // These paths are logged at DEBUG level (hidden by default) instead of INFO. isQuietPath := false @@ -177,10 +192,10 @@ func API(application *application.Application) (*echo.Echo, error) { } } - if isQuietPath && res.Status == 200 { - xlog.Debug("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status) + if isQuietPath && status == 200 { + xlog.Debug("HTTP request", "method", req.Method, "path", req.URL.Path, "status", status) } else { - xlog.Info("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status) + xlog.Info("HTTP request", "method", req.Method, "path", req.URL.Path, "status", status) } return err } diff --git a/core/http/endpoints/openai/transcription.go b/core/http/endpoints/openai/transcription.go index 2979ecd59..6312e3cb9 100644 --- a/core/http/endpoints/openai/transcription.go +++ b/core/http/endpoints/openai/transcription.go @@ -128,6 +128,15 @@ func TranscriptEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, app tr, err := backend.ModelTranscriptionWithOptions(req, ml, *config, appConfig) if err != nil { + // Log before returning so the underlying error survives. Echo's + // error handler turns this into a 500 with a generic body, which + // otherwise leaves operators chasing a silent failure — see e.g. + // distributed transcription, where the gRPC error from a remote + // node is the only signal of what actually went wrong. + xlog.Error("Transcription failed", + "model", config.Name, + "audio", dst, + "error", err) return err }