Skip to content

Commit 52591c0

Browse files
committed
fix: handle queries on non-existing table gracefully
Add new `TableNotFound` error and add json error message for this error. Closes #3697.
1 parent e2dd435 commit 52591c0

File tree

13 files changed

+74
-36
lines changed

13 files changed

+74
-36
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
2626
- #3795, Clarify `Accept: vnd.pgrst.object` error message - @steve-chavez
2727
- #3779, Always log the schema cache load time - @steve-chavez
2828
- #3706, Fix insert with `missing=default` uses default value of domain instead of column - @taimoorzaeem
29+
- #3697, Handle queries on non-existing table gracefully - @taimoorzaeem
2930

3031
### Changed
3132

docs/references/errors.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,10 @@ Related to a :ref:`schema_cache`. Most of the time, these errors are solved by :
294294
| | | in the ``columns`` query parameter is not found. |
295295
| PGRST204 | | |
296296
+---------------+-------------+-------------------------------------------------------------+
297+
| .. _pgrst205: | 404 | Caused when the :ref:`table specified <tables_views>` in |
298+
| | | the URI is not found. |
299+
| PGRST205 | | |
300+
+---------------+-------------+-------------------------------------------------------------+
297301

298302
.. _pgrst3**:
299303

src/PostgREST/ApiRequest/Types.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ data ApiRequestError
9191
| UnacceptableSchema [Text]
9292
| UnsupportedMethod ByteString
9393
| ColumnNotFound Text Text
94+
| TableNotFound Text
9495
| GucHeadersError
9596
| GucStatusError
9697
| OffLimitsChangesError Int64 Integer

src/PostgREST/Error.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ instance PgrstError ApiRequestError where
8686
status UnsupportedMethod{} = HTTP.status405
8787
status LimitNoOrderError = HTTP.status400
8888
status ColumnNotFound{} = HTTP.status400
89+
status TableNotFound{} = HTTP.status404
8990
status GucHeadersError = HTTP.status500
9091
status GucStatusError = HTTP.status500
9192
status OffLimitsChangesError{} = HTTP.status400
@@ -253,6 +254,9 @@ instance JSON.ToJSON ApiRequestError where
253254
toJSON (ColumnNotFound relName colName) = toJsonPgrstError
254255
SchemaCacheErrorCode04 ("Could not find the '" <> colName <> "' column of '" <> relName <> "' in the schema cache") Nothing Nothing
255256

257+
toJSON (TableNotFound relName) = toJsonPgrstError
258+
SchemaCacheErrorCode05 ("Could not find relation '" <> relName <> "' in the schema cache") Nothing Nothing
259+
256260
-- |
257261
-- If no relationship is found then:
258262
--
@@ -640,6 +644,7 @@ data ErrorCode
640644
| SchemaCacheErrorCode02
641645
| SchemaCacheErrorCode03
642646
| SchemaCacheErrorCode04
647+
| SchemaCacheErrorCode05
643648
-- JWT authentication errors
644649
| JWTErrorCode00
645650
| JWTErrorCode01
@@ -689,6 +694,7 @@ buildErrorCode code = case code of
689694
SchemaCacheErrorCode02 -> "PGRST202"
690695
SchemaCacheErrorCode03 -> "PGRST203"
691696
SchemaCacheErrorCode04 -> "PGRST204"
697+
SchemaCacheErrorCode05 -> "PGRST205"
692698

693699
JWTErrorCode00 -> "PGRST300"
694700
JWTErrorCode01 -> "PGRST301"

src/PostgREST/Plan.hs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import PostgREST.SchemaCache (SchemaCache (..))
5252
import PostgREST.SchemaCache.Identifiers (FieldName,
5353
QualifiedIdentifier (..),
5454
RelIdentifier (..),
55-
Schema)
55+
Schema, dumpQi)
5656
import PostgREST.SchemaCache.Relationship (Cardinality (..),
5757
Junction (..),
5858
Relationship (..),
@@ -152,14 +152,14 @@ dbActionPlan dbAct conf apiReq sCache = case dbAct of
152152

153153
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan
154154
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
155-
rPlan <- readPlan identifier conf sCache apiRequest
155+
rPlan <- readPlan False identifier conf sCache apiRequest
156156
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
157157
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
158158
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly identifier
159159

160160
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
161161
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
162-
rPlan <- readPlan identifier conf sCache apiRequest
162+
rPlan <- readPlan False identifier conf sCache apiRequest
163163
mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan
164164
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
165165
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
@@ -173,7 +173,7 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc
173173
proc@Function{..} <- mapLeft ApiRequestError $
174174
findProc identifier paramKeys (dbRoutines sCache) iContentMediaType (invMethod == Inv)
175175
let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations
176-
rPlan <- readPlan relIdentifier conf sCache apiRequest
176+
rPlan <- readPlan True relIdentifier conf sCache apiRequest
177177
let args = case (invMethod, iContentMediaType) of
178178
(InvRead _, _) -> DirectArgs $ toRpcParams proc qsParams'
179179
(Inv, MTUrlEncoded) -> DirectArgs $ maybe mempty (toRpcParams proc . payArray) iPayload
@@ -320,8 +320,8 @@ resolveQueryInputField ctx field = withTextParse ctx $ resolveTypeOrUnknown ctx
320320
-- | Builds the ReadPlan tree on a number of stages.
321321
-- | Adds filters, order, limits on its respective nodes.
322322
-- | Adds joins conditions obtained from resource embedding.
323-
readPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error ReadPlanTree
324-
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregates} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
323+
readPlan :: Bool -> QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error ReadPlanTree
324+
readPlan fromCallPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregates} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
325325
let
326326
-- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.
327327
ctx = ResolverContext dbTables dbRepresentations qi "json"
@@ -340,7 +340,8 @@ readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregate
340340
addLogicTrees ctx apiRequest =<<
341341
addRanges apiRequest =<<
342342
addOrders ctx apiRequest =<<
343-
addFilters ctx apiRequest (initReadRequest ctx $ QueryParams.qsSelect $ iQueryParams apiRequest)
343+
addFilters ctx apiRequest =<<
344+
searchTable fromCallPlan qi dbTables (initReadRequest ctx $ QueryParams.qsSelect $ iQueryParams apiRequest)
344345

345346
-- Build the initial read plan tree
346347
initReadRequest :: ResolverContext -> [Tree SelectItem] -> ReadPlanTree
@@ -738,6 +739,14 @@ validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
738739
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left AggregatesNotAllowed
739740
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
740741

742+
-- We only search for the table when readPlan is not called from call plan. This
743+
-- is because we reuse readPlan in callReadPlan to search for function name
744+
searchTable :: Bool -> QualifiedIdentifier -> TablesMap -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
745+
searchTable fromCallPlan qi tbls readPlanTree =
746+
case (fromCallPlan, HM.lookup qi tbls) of
747+
(False, Nothing) -> Left (TableNotFound $ dumpQi qi)
748+
_ -> Right readPlanTree
749+
741750
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
742751
addFilters ctx ApiRequest{..} rReq =
743752
foldr addFilterToNode (Right rReq) flts
@@ -961,7 +970,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
961970
typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns
962971

963972
resolveOrError :: ResolverContext -> Maybe Table -> FieldName -> Either ApiRequestError CoercibleField
964-
resolveOrError _ Nothing _ = Left NotFound
973+
resolveOrError ResolverContext{qi} Nothing _ = Left $ TableNotFound $ dumpQi qi
965974
resolveOrError ctx (Just table) field =
966975
case resolveTableFieldName table field of
967976
CoercibleField{cfIRType=""} -> Left $ ColumnNotFound (tableName table) field

test/io/test_big_schema.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,6 @@ def test_should_not_fail_with_stack_overflow(defaultenv):
6565

6666
with run(env=env, wait_max_seconds=30) as postgrest:
6767
response = postgrest.session.get("/unknown-table?select=unknown-rel(*)")
68-
assert response.status_code == 400
68+
assert response.status_code == 404
6969
data = response.json()
70-
assert data["code"] == "PGRST200"
70+
assert data["code"] == "PGRST205"

test/io/test_io.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -973,11 +973,10 @@ def test_log_level(level, defaultenv):
973973
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 - "" "python-requests/.+"',
974974
output[2],
975975
)
976+
977+
assert len(output) == 5
976978
assert "Connection" and "is available" in output[3]
977-
assert "Connection" and "is available" in output[4]
978-
assert "Connection" and "is used" in output[5]
979-
assert "Connection" and "is used" in output[6]
980-
assert len(output) == 7
979+
assert "Connection" and "is used" in output[4]
981980

982981

983982
def test_no_pool_connection_required_on_bad_http_logic(defaultenv):

test/spec/Feature/ConcurrentSpec.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ spec =
2727
`shouldRespondWith` [json|
2828
{ "hint": null,
2929
"details":null,
30-
"code":"42P01",
31-
"message":"relation \"test.fakefake\" does not exist"
30+
"code":"PGRST205",
31+
"message":"Could not find relation 'test.fakefake' in the schema cache"
3232
} |]
3333
{ matchStatus = 404
3434
, matchHeaders = []

test/spec/Feature/Query/DeleteSpec.hs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,15 @@ spec =
109109

110110
context "totally unknown route" $
111111
it "fails with 404" $
112-
request methodDelete "/foozle?id=eq.101" [] "" `shouldRespondWith` 404
112+
request methodDelete "/foozle?id=eq.101" [] ""
113+
`shouldRespondWith` [json|
114+
{ "hint": null, "details":null,
115+
"code":"PGRST205",
116+
"message":"Could not find relation 'test.foozle' in the schema cache"
117+
} |]
118+
{ matchStatus = 404
119+
, matchHeaders = []
120+
}
113121

114122
context "table with limited privileges" $ do
115123
it "fails deleting the row when return=representation and selecting all the columns" $

test/spec/Feature/Query/InsertSpec.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ spec actualPgVersion = do
477477
{"id": 204, "body": "yyy"},
478478
{"id": 205, "body": "zzz"}]|]
479479
`shouldRespondWith`
480-
[json|{} |]
480+
[json|{"code":"PGRST205","details":null,"hint":null,"message":"Could not find relation 'test.garlic' in the schema cache"} |]
481481
{ matchStatus = 404
482482
, matchHeaders = []
483483
}

0 commit comments

Comments
 (0)