@@ -10,6 +10,31 @@ import (
1010 "strings"
1111
1212 "github.com/iancoleman/strcase"
13+
14+ "github.com/formbricks/hub/internal/models"
15+ )
16+
17+ const (
18+ // ProblemTypeBadRequest identifies malformed request problems.
19+ ProblemTypeBadRequest = "https://hub.formbricks.com/problems/bad-request"
20+ // ProblemTypeValidationError identifies request validation problems.
21+ ProblemTypeValidationError = "https://hub.formbricks.com/problems/validation-error"
22+ // ProblemTypeClientError identifies unclassified client-side request problems.
23+ ProblemTypeClientError = "https://hub.formbricks.com/problems/client-error"
24+ // ProblemTypeUnauthorized identifies authentication problems.
25+ ProblemTypeUnauthorized = "https://hub.formbricks.com/problems/unauthorized"
26+ // ProblemTypeNotFound identifies missing resource problems.
27+ ProblemTypeNotFound = "https://hub.formbricks.com/problems/not-found"
28+ // ProblemTypeConflict identifies resource conflict problems.
29+ ProblemTypeConflict = "https://hub.formbricks.com/problems/conflict"
30+ // ProblemTypeForbidden identifies authorization problems.
31+ ProblemTypeForbidden = "https://hub.formbricks.com/problems/forbidden"
32+ // ProblemTypeMethodNotAllowed identifies unsupported HTTP method problems.
33+ ProblemTypeMethodNotAllowed = "https://hub.formbricks.com/problems/method-not-allowed"
34+ // ProblemTypeServiceUnavailable identifies temporary dependency problems.
35+ ProblemTypeServiceUnavailable = "https://hub.formbricks.com/problems/service-unavailable"
36+ // ProblemTypeInternalServerError identifies unexpected server problems.
37+ ProblemTypeInternalServerError = "https://hub.formbricks.com/problems/internal-server-error"
1338)
1439
1540// ErrorDetail represents a single error detail in RFC 7807 Problem Details.
@@ -32,12 +57,47 @@ type ProblemDetails struct {
3257// RespondError writes an RFC 7807 Problem Details error response.
3358func RespondError (w http.ResponseWriter , statusCode int , title , detail string ) {
3459 problem := ProblemDetails {
35- Type : "about:blank" ,
60+ Type : problemTypeForStatus ( statusCode ) ,
3661 Title : title ,
3762 Status : statusCode ,
3863 Detail : detail ,
3964 }
4065
66+ respondProblem (w , statusCode , problem )
67+ }
68+
69+ // RespondInvalidRequestBody writes a 400 response for JSON request body decoding failures.
70+ func RespondInvalidRequestBody (w http.ResponseWriter , err error ) {
71+ problemType := jsonDecodeProblemType (err )
72+
73+ problem := ProblemDetails {
74+ Type : problemType ,
75+ Title : jsonDecodeProblemTitle (problemType ),
76+ Status : http .StatusBadRequest ,
77+ Detail : JSONDecodeErrorDetail (err ),
78+ Errors : JSONDecodeErrorDetails (err ),
79+ }
80+
81+ respondProblem (w , http .StatusBadRequest , problem )
82+ }
83+
84+ func jsonDecodeProblemType (err error ) string {
85+ if _ , ok := invalidFieldTypeErrorDetail (err ); ok {
86+ return ProblemTypeValidationError
87+ }
88+
89+ return ProblemTypeBadRequest
90+ }
91+
92+ func jsonDecodeProblemTitle (problemType string ) string {
93+ if problemType == ProblemTypeValidationError {
94+ return "Validation Error"
95+ }
96+
97+ return "Bad Request"
98+ }
99+
100+ func respondProblem (w http.ResponseWriter , statusCode int , problem ProblemDetails ) {
41101 w .Header ().Set ("Content-Type" , "application/problem+json" )
42102 w .WriteHeader (statusCode )
43103
@@ -46,6 +106,33 @@ func RespondError(w http.ResponseWriter, statusCode int, title, detail string) {
46106 }
47107}
48108
109+ func problemTypeForStatus (statusCode int ) string {
110+ switch statusCode {
111+ case http .StatusBadRequest :
112+ return ProblemTypeBadRequest
113+ case http .StatusUnauthorized :
114+ return ProblemTypeUnauthorized
115+ case http .StatusForbidden :
116+ return ProblemTypeForbidden
117+ case http .StatusMethodNotAllowed :
118+ return ProblemTypeMethodNotAllowed
119+ case http .StatusNotFound :
120+ return ProblemTypeNotFound
121+ case http .StatusConflict :
122+ return ProblemTypeConflict
123+ case http .StatusServiceUnavailable :
124+ return ProblemTypeServiceUnavailable
125+ case http .StatusInternalServerError :
126+ return ProblemTypeInternalServerError
127+ default :
128+ if statusCode >= http .StatusBadRequest && statusCode < http .StatusInternalServerError {
129+ return ProblemTypeClientError
130+ }
131+
132+ return ProblemTypeInternalServerError
133+ }
134+ }
135+
49136// RespondBadRequest writes a 400 Bad Request error response.
50137func RespondBadRequest (w http.ResponseWriter , detail string ) {
51138 RespondError (w , http .StatusBadRequest , "Bad Request" , detail )
@@ -59,6 +146,10 @@ func JSONDecodeErrorDetail(err error) string {
59146 return "Invalid request body"
60147 }
61148
149+ if detail , ok := invalidFieldTypeErrorDetail (err ); ok {
150+ return detail .Message
151+ }
152+
62153 var syntaxErr * json.SyntaxError
63154 if errors .As (err , & syntaxErr ) {
64155 return "Invalid JSON: " + err .Error ()
@@ -78,6 +169,32 @@ func JSONDecodeErrorDetail(err error) string {
78169 return "Invalid request body"
79170}
80171
172+ // JSONDecodeErrorDetails returns field-level details for JSON request body decoding failures.
173+ func JSONDecodeErrorDetails (err error ) []ErrorDetail {
174+ if detail , ok := invalidFieldTypeErrorDetail (err ); ok {
175+ return []ErrorDetail {detail }
176+ }
177+
178+ return nil
179+ }
180+
181+ func invalidFieldTypeErrorDetail (err error ) (ErrorDetail , bool ) {
182+ var invalidFieldType * models.InvalidFieldTypeError
183+ if ! errors .As (err , & invalidFieldType ) {
184+ return ErrorDetail {}, false
185+ }
186+
187+ return ErrorDetail {
188+ Location : "field_type" ,
189+ Message : fmt .Sprintf (
190+ "field_type has invalid value %q; must be one of: %s" ,
191+ invalidFieldType .Value ,
192+ models .ValidFieldTypeValuesString (),
193+ ),
194+ Value : invalidFieldType .Value ,
195+ }, true
196+ }
197+
81198// fieldNameForAPI converts a struct field path (e.g. "TenantID" or "X.Y") to API-style snake_case.
82199func fieldNameForAPI (fieldPath string ) string {
83200 if i := strings .LastIndex (fieldPath , "." ); i >= 0 && i + 1 < len (fieldPath ) {
0 commit comments