From a19fd3af64bba370b32a05d3081bbd1fd1188364 Mon Sep 17 00:00:00 2001 From: phatlc Date: Sun, 6 Sep 2026 00:32:26 +0700 Subject: [PATCH] fix(udm): return ProblemDetails for unmatched resource URIs The SDM and UEAU path dispatchers fell through to a text/plain "404 page not found" when a request reached the API but named no resource the NF defines (e.g. DELETE /nudm-sdm/v2/{supi} or GET /nudm-sdm/v2/{supi}/no-such-resource). TS 29.500 table 5.2.7.2-1 defines this case as 404 with a ProblemDetails whose cause is RESOURCE_URI_STRUCTURE_NOT_FOUND, and TS 29.501 4.8.2 only allows application/problem+json for error bodies. Answer with the same ProblemDetails pattern the surrounding handlers already use; the status code is unchanged. Part of https://github.com/free5gc/free5gc/issues/1157 --- internal/sbi/api_subscriberdatamanagement.go | 22 ++++- .../sbi/api_subscriberdatamanagement_test.go | 82 +++++++++++++++++++ internal/sbi/api_ueauthentication.go | 4 +- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/internal/sbi/api_subscriberdatamanagement.go b/internal/sbi/api_subscriberdatamanagement.go index 9fe29e1..2baabb4 100644 --- a/internal/sbi/api_subscriberdatamanagement.go +++ b/internal/sbi/api_subscriberdatamanagement.go @@ -740,6 +740,22 @@ func (s *Server) HandleUpuAck(c *gin.Context) { c.JSON(http.StatusNotImplemented, gin.H{}) } +// respondResourceURIStructureNotFound answers a request that reached the API +// but names no resource this NF defines, as TS 29.500 table 5.2.7.2-1 +// requires: 404 with a ProblemDetails whose cause is +// RESOURCE_URI_STRUCTURE_NOT_FOUND. +func respondResourceURIStructureNotFound(c *gin.Context) { + problemDetails := models.ProblemDetails{ + Title: "Resource URI structure not found", + Status: http.StatusNotFound, + Detail: c.Request.Method + " " + c.Request.URL.Path + " does not name a resource of this API", + Cause: "RESOURCE_URI_STRUCTURE_NOT_FOUND", + } + c.Set(sbi.IN_PB_DETAILS_CTX_STR, problemDetails.Cause) + c.Header("Content-Type", "application/problem+json") + c.JSON(int(problemDetails.Status), problemDetails) +} + func (s *Server) OneLayerPathHandlerFunc(c *gin.Context) { supi := c.Param("supi") oneLayerPathRouter := s.getOneLayerRoutes() @@ -756,7 +772,7 @@ func (s *Server) OneLayerPathHandlerFunc(c *gin.Context) { return } - c.String(http.StatusNotFound, "404 page not found") + respondResourceURIStructureNotFound(c) } func (s *Server) TwoLayerPathHandlerFunc(c *gin.Context) { @@ -799,7 +815,7 @@ func (s *Server) TwoLayerPathHandlerFunc(c *gin.Context) { } } - c.String(http.StatusNotFound, "404 page not found") + respondResourceURIStructureNotFound(c) } func pathPatternMatches(pattern, path string) bool { @@ -881,7 +897,7 @@ func (s *Server) ThreeLayerPathHandlerFunc(c *gin.Context) { return } - c.String(http.StatusNotFound, "404 page not found") + respondResourceURIStructureNotFound(c) } func (s *Server) getOneLayerRoutes() []Route { diff --git a/internal/sbi/api_subscriberdatamanagement_test.go b/internal/sbi/api_subscriberdatamanagement_test.go index 2cce3aa..99120f6 100644 --- a/internal/sbi/api_subscriberdatamanagement_test.go +++ b/internal/sbi/api_subscriberdatamanagement_test.go @@ -19,6 +19,7 @@ import ( "github.com/free5gc/udm/internal/sbi/consumer" "github.com/free5gc/udm/internal/sbi/processor" "github.com/free5gc/udm/pkg/app" + "github.com/free5gc/util/metrics/sbi" "github.com/free5gc/util/validator" ) @@ -161,6 +162,87 @@ func TestTwoLayerPathHandlerMatchesPathAndMethod(t *testing.T) { } } +func TestPathHandlersRejectUnmatchedResourceURIs(t *testing.T) { + server := &Server{} + const supi = "imsi-208930000000003" + tests := []struct { + name string + method string + path string + params gin.Params + handler func(*gin.Context) + }{ + { + name: "sdm one layer wrong method", + method: http.MethodDelete, + path: "/nudm-sdm/v2/" + supi, + params: gin.Params{{Key: "supi", Value: supi}}, + handler: server.OneLayerPathHandlerFunc, + }, + { + name: "sdm two layer unknown resource", + method: http.MethodGet, + path: "/nudm-sdm/v2/" + supi + "/no-such-resource", + params: gin.Params{{Key: "supi", Value: supi}, {Key: "subscriptionId", Value: "no-such-resource"}}, + handler: server.TwoLayerPathHandlerFunc, + }, + { + name: "sdm two layer wrong method", + method: http.MethodPost, + path: "/nudm-sdm/v2/" + supi + "/am-data", + params: gin.Params{{Key: "supi", Value: supi}, {Key: "subscriptionId", Value: "am-data"}}, + handler: server.TwoLayerPathHandlerFunc, + }, + { + name: "sdm three layer unknown resource", + method: http.MethodGet, + path: "/nudm-sdm/v2/" + supi + "/am-data/no-such-resource", + params: gin.Params{ + {Key: "supi", Value: supi}, + {Key: "subscriptionId", Value: "am-data"}, + {Key: "thirdLayer", Value: "no-such-resource"}, + }, + handler: server.ThreeLayerPathHandlerFunc, + }, + { + name: "ueau two layer unknown resource", + method: http.MethodGet, + path: "/nudm-ueau/v1/" + supi + "/no-such-resource", + params: gin.Params{{Key: "supi", Value: supi}, {Key: "twoLayer", Value: "no-such-resource"}}, + handler: server.UEAUTwoLayerPathHandlerFunc, + }, + { + name: "ueau three layer unknown resource", + method: http.MethodPost, + path: "/nudm-ueau/v1/" + supi + "/no-such-resource/generate-av", + params: gin.Params{ + {Key: "supi", Value: supi}, + {Key: "twoLayer", Value: "no-such-resource"}, + {Key: "thirdLayer", Value: "generate-av"}, + }, + handler: server.UEAUThreeLayerPathHandlerFunc, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + recorder, c := newSDMTestContext(t, tt.method, tt.path, "") + c.Params = tt.params + + require.NotPanics(t, func() { tt.handler(c) }) + require.Equal(t, http.StatusNotFound, recorder.Code) + require.Equal(t, "application/problem+json", recorder.Header().Get("Content-Type")) + + var problem models.ProblemDetails + require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &problem)) + require.Equal(t, int32(http.StatusNotFound), problem.Status) + require.Equal(t, "RESOURCE_URI_STRUCTURE_NOT_FOUND", problem.Cause) + require.Contains(t, problem.Detail, tt.path) + require.Equal(t, problem.Cause, c.GetString(sbi.IN_PB_DETAILS_CTX_STR)) + }) + } +} + func TestIsValidSDMSubscriptionID(t *testing.T) { tests := []struct { name string diff --git a/internal/sbi/api_ueauthentication.go b/internal/sbi/api_ueauthentication.go index ab61b3f..58ebc1e 100644 --- a/internal/sbi/api_ueauthentication.go +++ b/internal/sbi/api_ueauthentication.go @@ -218,7 +218,7 @@ func (s *Server) UEAUTwoLayerPathHandlerFunc(c *gin.Context) { return } - c.String(http.StatusNotFound, "404 page not found") + respondResourceURIStructureNotFound(c) } func (s *Server) UEAUThreeLayerPathHandlerFunc(c *gin.Context) { @@ -254,5 +254,5 @@ func (s *Server) UEAUThreeLayerPathHandlerFunc(c *gin.Context) { return } - c.String(http.StatusNotFound, "404 page not found") + respondResourceURIStructureNotFound(c) }