feat: add Proxy-Status header for better error response
This commit is contained in:
@@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #3041, Allow spreading one-to-many and many-to-many embedded resources - @laurenceisla
|
- #3041, Allow spreading one-to-many and many-to-many embedded resources - @laurenceisla
|
||||||
+ The selected columns in the embedded resources are aggregated into arrays
|
+ The selected columns in the embedded resources are aggregated into arrays
|
||||||
+ Aggregates are not supported
|
+ Aggregates are not supported
|
||||||
|
- #2967, Add `Proxy-Status` header for better error response - @taimoorzaeem
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ Errors
|
|||||||
|
|
||||||
PostgREST error messages follow the PostgreSQL error structure. It includes ``MESSAGE``, ``DETAIL``, ``HINT``, ``ERRCODE`` and will add an HTTP status code to the response.
|
PostgREST error messages follow the PostgreSQL error structure. It includes ``MESSAGE``, ``DETAIL``, ``HINT``, ``ERRCODE`` and will add an HTTP status code to the response.
|
||||||
|
|
||||||
|
.. _postgresql_errors:
|
||||||
|
|
||||||
Errors from PostgreSQL
|
Errors from PostgreSQL
|
||||||
======================
|
======================
|
||||||
|
|
||||||
@@ -348,6 +350,8 @@ Internal errors. If you encounter any of these, you may have stumbled on a Postg
|
|||||||
| PGRSTX00 | | |
|
| PGRSTX00 | | |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
|
|
||||||
|
.. _custom_errors:
|
||||||
|
|
||||||
Custom Errors
|
Custom Errors
|
||||||
=============
|
=============
|
||||||
|
|
||||||
@@ -444,3 +448,24 @@ For non standard HTTP status, you can optionally add ``status_text`` to describe
|
|||||||
detail = '{"status":419,"status_text":"Page Expired","headers":{"X-Powered-By":"Nerd Rage"}}';
|
detail = '{"status":419,"status_text":"Page Expired","headers":{"X-Powered-By":"Nerd Rage"}}';
|
||||||
|
|
||||||
If PostgREST can't parse the JSON objects ``message`` and ``detail``, it will throw a ``PGRST121`` error. See :ref:`Errors from PostgREST<pgrst1**>`.
|
If PostgREST can't parse the JSON objects ``message`` and ``detail``, it will throw a ``PGRST121`` error. See :ref:`Errors from PostgREST<pgrst1**>`.
|
||||||
|
|
||||||
|
.. _proxy-status_header:
|
||||||
|
|
||||||
|
Proxy-Status Header
|
||||||
|
===================
|
||||||
|
|
||||||
|
For error cases, the standard `Proxy-Status <https://www.rfc-editor.org/rfc/rfc9209.html#name-the-proxy-status-http-field>`_ header is returned with the error code. The error code comes from either :ref:`PostgREST <pgrst_errors>`, :ref:`PostgreSQL <postgresql_errors>` or :ref:`Custom <custom_errors>` errors. This is useful when doing ``HEAD`` requests where the HTTP status is not descriptive enough.
|
||||||
|
|
||||||
|
For example, doing a request on a table with high count (say 30_000_000), we get:
|
||||||
|
|
||||||
|
.. code-block:: http
|
||||||
|
|
||||||
|
HEAD /table HTTP/1.1
|
||||||
|
Prefer: count=exact
|
||||||
|
|
||||||
|
.. code-block:: http
|
||||||
|
|
||||||
|
HTTP/1.1 500 Internal Server Error
|
||||||
|
Proxy-Status: PostgREST; error=57014
|
||||||
|
|
||||||
|
The PostgreSQL error code ``57014`` (`ref <https://www.postgresql.org/docs/current/errcodes-appendix.html>`_) reveals that the error is due to a short ``statement_timeout`` value.
|
||||||
|
|||||||
@@ -236,6 +236,11 @@ You can enable tracing HTTP requests by setting :ref:`server-trace-header`. Spec
|
|||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
X-Request-Id: 123
|
X-Request-Id: 123
|
||||||
|
|
||||||
|
Proxy-Status Header
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
See :ref:`proxy-status_header`.
|
||||||
|
|
||||||
.. _server-timing_header:
|
.. _server-timing_header:
|
||||||
|
|
||||||
Server-Timing Header
|
Server-Timing Header
|
||||||
|
|||||||
@@ -478,6 +478,9 @@ instance PgrstError PgError where
|
|||||||
then [("WWW-Authenticate", "Bearer") :: Header]
|
then [("WWW-Authenticate", "Bearer") :: Header]
|
||||||
else mempty
|
else mempty
|
||||||
|
|
||||||
|
proxyStatusHeader :: Text -> Header
|
||||||
|
proxyStatusHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code')
|
||||||
|
|
||||||
instance JSON.ToJSON PgError where
|
instance JSON.ToJSON PgError where
|
||||||
toJSON (PgError _ usageError) = toJsonPgrstError
|
toJSON (PgError _ usageError) = toJsonPgrstError
|
||||||
(code usageError) (message usageError) (details usageError) (hint usageError)
|
(code usageError) (message usageError) (details usageError) (hint usageError)
|
||||||
@@ -625,10 +628,10 @@ instance PgrstError Error where
|
|||||||
status NoSchemaCacheError = HTTP.status503
|
status NoSchemaCacheError = HTTP.status503
|
||||||
status (PgErr err) = status err
|
status (PgErr err) = status err
|
||||||
|
|
||||||
headers (ApiRequestError err) = headers err
|
headers (ApiRequestError err) = proxyStatusHeader (code err) : headers err
|
||||||
headers (JwtErr err) = headers err
|
headers (JwtErr err) = proxyStatusHeader (code err) : headers err
|
||||||
headers (PgErr err) = headers err
|
headers (PgErr err) = proxyStatusHeader (code err) : headers err
|
||||||
headers _ = mempty
|
headers err@NoSchemaCacheError = proxyStatusHeader (code err) : mempty
|
||||||
|
|
||||||
instance JSON.ToJSON Error where
|
instance JSON.ToJSON Error where
|
||||||
toJSON err = toJsonPgrstError
|
toJSON err = toJsonPgrstError
|
||||||
|
|||||||
@@ -1830,3 +1830,23 @@ def test_log_pool_req_observation(level, defaultenv):
|
|||||||
else:
|
else:
|
||||||
output = postgrest.read_stdout(nlines=4)
|
output = postgrest.read_stdout(nlines=4)
|
||||||
assert len(output) == 0
|
assert len(output) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_proxy_status_header(defaultenv, metapostgrest):
|
||||||
|
"Test Proxy-Status header in statement timeout error"
|
||||||
|
|
||||||
|
role = "timeout_authenticator"
|
||||||
|
set_statement_timeout(metapostgrest, role, 1000) # 1 second
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGUSER": role,
|
||||||
|
"PGRST_DB_ANON_ROLE": role,
|
||||||
|
}
|
||||||
|
|
||||||
|
with run(env=env) as postgrest:
|
||||||
|
response = postgrest.session.get("/rpc/sleep?seconds=2")
|
||||||
|
assert response.status_code == 500
|
||||||
|
assert response.headers["Proxy-Status"] == "PostgREST; error=57014"
|
||||||
|
data = response.json()
|
||||||
|
assert data["message"] == "canceling statement due to statement timeout"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Test.Hspec.Wai
|
|||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
|
import SpecHelper
|
||||||
|
|
||||||
pgErrorCodeMapping :: SpecWith ((), Application)
|
pgErrorCodeMapping :: SpecWith ((), Application)
|
||||||
pgErrorCodeMapping = do
|
pgErrorCodeMapping = do
|
||||||
@@ -26,3 +27,45 @@ pgErrorCodeMapping = do
|
|||||||
"hint": "Increase the configuration parameter \"max_stack_depth\" (currently 2048kB), after ensuring the platform's stack depth limit is adequate.",
|
"hint": "Increase the configuration parameter \"max_stack_depth\" (currently 2048kB), after ensuring the platform's stack depth limit is adequate.",
|
||||||
"message": "stack depth limit exceeded"}|]
|
"message": "stack depth limit exceeded"}|]
|
||||||
{ matchStatus = 500 }
|
{ matchStatus = 500 }
|
||||||
|
|
||||||
|
context "includes the proxy-status header on the response" $ do
|
||||||
|
it "works with ApiRequest error" $
|
||||||
|
get "/invalid/nested/paths"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
|
||||||
|
{ matchStatus = 404
|
||||||
|
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST125"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with SchemaCache error" $
|
||||||
|
get "/non_existent_table"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.json_table'","message":"Could not find the table 'test.non_existent_table' in the schema cache"} |]
|
||||||
|
{ matchStatus = 404
|
||||||
|
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST205"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with Jwt error" $ do
|
||||||
|
let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
|
||||||
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"message":"Expected 3 parts in JWT; got 2","code":"PGRST301","hint":null,"details":null} |]
|
||||||
|
{ matchStatus = 401
|
||||||
|
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST301"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with raise sqlstate custom error" $
|
||||||
|
get "/rpc/raise_pt402"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"code":"PT402","details":"Quota exceeded","hint":"Upgrade your plan","message":"Payment Required"} |]
|
||||||
|
{ matchStatus = 402
|
||||||
|
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PT402"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with sqlstate PGRST custom error" $
|
||||||
|
get "/rpc/raise_sqlstate_test1"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"code":"123","details":"DEF","hint":"XYZ","message":"ABC"} |]
|
||||||
|
{ matchStatus = 332
|
||||||
|
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=123"]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user