From e95e815483c542f430cf8aa8df15419143f400fa Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 11 Feb 2026 09:40:41 -0500 Subject: [PATCH] fix: don't hide async exceptions in logs Fixes #4646. Using the repro on #4646, this now produces the log: ``` 11/Feb/2026:09:40:08 -0500: Warp server error: stack overflow ``` When: ``` $ curl localhost:3000/ curl: (52) Empty reply from server ``` --- CHANGELOG.md | 2 ++ src/PostgREST/App.hs | 30 ++++++++++++++++++++++++++---- src/PostgREST/Observation.hs | 3 +++ test/io/test_big_schema.py | 20 ++++++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09aa9af85..781b5279c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ All notable changes to this project will be documented in this file. From versio ### Fixed +- Don't hide async exceptions in logs by @stevechavez in #4646 + ### Changed - Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359 diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 411694fd1..64e31b194 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -9,18 +9,24 @@ Some of its functionality includes: - Producing HTTP Headers according to RFCs. - Content Negotiation -} -{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE ViewPatterns #-} module PostgREST.App ( postgrest , run ) where +import GHC.IO.Exception (IOErrorType (..)) +import System.IO.Error (ioeGetErrorType) + import Control.Monad.Except (liftEither) import Data.Either.Combinators (mapLeft, whenLeft) import Data.Maybe (fromJust) import Data.String (IsString (..)) -import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort, +import Network.Wai.Handler.Warp (defaultSettings, setHost, + setOnException, setPort, setServerName) import qualified Data.Text.Encoding as T @@ -63,7 +69,6 @@ type Handler = ExceptT Error run :: AppState -> IO () run appState = do - let observer = AppState.getObserver appState conf@AppConfig{..} <- AppState.getConfig appState AppState.schemaCacheLoader appState -- Loads the initial SchemaCache @@ -79,7 +84,24 @@ run appState = do address <- resolveSocketToAddress (AppState.getSocketREST appState) observer $ AppServerAddressObs address - Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app + Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) (AppState.getSocketREST appState) app + where + observer = AppState.getObserver appState + + onWarpException :: Maybe Wai.Request -> SomeException -> IO () + onWarpException _ ex = + when (shouldDisplayException ex) $ + observer $ WarpErrorObs $ show ex + + -- Similar to wai defaultShouldDisplayException in + -- https://github.com/yesodweb/wai//blob/8c3882c60f6abe043889fc20c7efd3fa9747fa4a/warp/Network/Wai/Handler/Warp/Settings.hs#L251-L258 + -- but without omitting AsyncException since it's important to log for ThreadKilled, StackOverflow and other cases. + -- We want to reuse this to avoid flooding the logs for some transient failure cases. + shouldDisplayException :: SomeException -> Bool + shouldDisplayException se + | Just (_ :: Warp.InvalidRequest) <- fromException se = False + | Just (ioeGetErrorType -> et) <- fromException se, et == ResourceVanished || et == InvalidArgument = False + | otherwise = True serverSettings :: AppConfig -> Warp.Settings serverSettings AppConfig{..} = diff --git a/src/PostgREST/Observation.hs b/src/PostgREST/Observation.hs index 96a8a30ef..84f39fe8b 100644 --- a/src/PostgREST/Observation.hs +++ b/src/PostgREST/Observation.hs @@ -65,6 +65,7 @@ data Observation | PoolRequestFullfilled | JwtCacheLookup Bool | JwtCacheEviction + | WarpErrorObs Text data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01 @@ -161,6 +162,8 @@ observationMessage = \case "Looked up a JWT in JWT cache" JwtCacheEviction -> "Evicted entry from JWT cache" + WarpErrorObs txt -> + "Warp server error: " <> txt where showMillis :: Double -> Text showMillis x = toS $ showFFloat (Just 1) x "" diff --git a/test/io/test_big_schema.py b/test/io/test_big_schema.py index c4c4f9a78..e764bd755 100644 --- a/test/io/test_big_schema.py +++ b/test/io/test_big_schema.py @@ -3,6 +3,7 @@ import re import pytest +import requests from postgrest import run @@ -54,6 +55,25 @@ def test_openapi_in_big_schema(defaultenv): assert response.status_code == 200 +def test_stackoverflow_is_logged(defaultenv): + "Stack overflow errors should be logged with the Warp error message" + + env = { + **defaultenv, + "PGRST_DB_SCHEMAS": "apflora", + "PGRST_DB_ANON_ROLE": "postgrest_test_anonymous", + } + + with run(env=env, wait_max_seconds=30, no_startup_stdout=False) as postgrest: + with pytest.raises(requests.exceptions.ConnectionError): + postgrest.session.get("/") + + output = postgrest.read_stdout(nlines=10) + output.extend(postgrest.read_stdout(nlines=10)) + + assert any("Warp server error: stack overflow" in line for line in output) + + # See: https://github.com/PostgREST/postgrest/issues/3329 def test_should_not_fail_with_stack_overflow(defaultenv): "requesting a non-existent relationship should not fail with stack overflow due to fuzzy search of candidates"