1919
2020namespace phpMyFAQ ;
2121
22+ use phpMyFAQ \Api \ProblemDetails ;
2223use phpMyFAQ \Controller \Exception \ForbiddenException ;
2324use phpMyFAQ \Core \Exception ;
2425use Symfony \Component \DependencyInjection \ContainerInterface ;
@@ -139,15 +140,14 @@ private function handleRequest(
139140 $ response ->setStatusCode (Response::HTTP_OK );
140141 $ response = call_user_func_array ($ controller , $ arguments );
141142 } catch (ResourceNotFoundException $ exception ) {
142- // For API requests, return simple text/ JSON response
143+ // For API requests, return RFC 7807 JSON response
143144 if ($ this ->isApiContext ) {
144- $ message = Environment::isDebugMode ()
145- ? $ this ->formatExceptionMessage (
146- template: 'Not Found: :message at line :line at :file ' ,
147- exception: $ exception ,
148- )
149- : 'Not Found ' ;
150- $ response = new Response (content: $ message , status: Response::HTTP_NOT_FOUND );
145+ $ response = $ this ->createProblemDetailsResponse (
146+ request: $ request ,
147+ status: Response::HTTP_NOT_FOUND ,
148+ exception: $ exception ,
149+ defaultDetail: 'The requested resource was not found. ' ,
150+ );
151151 } else {
152152 // For web requests, forward to the PageNotFoundController
153153 try {
@@ -170,31 +170,51 @@ private function handleRequest(
170170 $ response = new Response (content: $ message , status: Response::HTTP_NOT_FOUND );
171171 }
172172 }
173- } catch (UnauthorizedHttpException ) {
174- $ response = new RedirectResponse (url: './login ' );
175- if (str_contains (haystack: $ urlMatcher ->getContext ()->getBaseUrl (), needle: '/api ' )) {
176- $ response = new Response (
177- content: json_encode (value: ['error ' => 'Unauthorized access ' ]),
173+ } catch (UnauthorizedHttpException $ exception ) {
174+ if ($ this ->isApiContext ) {
175+ $ response = $ this ->createProblemDetailsResponse (
176+ request: $ request ,
178177 status: Response::HTTP_UNAUTHORIZED ,
179- headers: ['Content-Type ' => 'application/json ' ],
178+ exception: $ exception ,
179+ defaultDetail: 'Unauthorized access. ' ,
180180 );
181+ } else {
182+ $ response = new RedirectResponse (url: './login ' );
181183 }
182184 } catch (ForbiddenException $ exception ) {
183- $ message = Environment::isDebugMode ()
184- ? $ this ->formatExceptionMessage (
185- template: 'An error occurred: :message at line :line at :file ' ,
185+ if ($ this ->isApiContext ) {
186+ $ response = $ this ->createProblemDetailsResponse (
187+ request: $ request ,
188+ status: Response::HTTP_FORBIDDEN ,
186189 exception: $ exception ,
187- )
188- : 'Bad Request ' ;
189- $ response = new Response (content: $ message , status: Response::HTTP_FORBIDDEN );
190+ defaultDetail: 'Access to this resource is forbidden. ' ,
191+ );
192+ } else {
193+ $ message = Environment::isDebugMode ()
194+ ? $ this ->formatExceptionMessage (
195+ template: 'An error occurred: :message at line :line at :file ' ,
196+ exception: $ exception ,
197+ )
198+ : 'Forbidden ' ;
199+ $ response = new Response (content: $ message , status: Response::HTTP_FORBIDDEN );
200+ }
190201 } catch (BadRequestException $ exception ) {
191- $ message = Environment::isDebugMode ()
192- ? $ this ->formatExceptionMessage (
193- template: 'An error occurred: :message at line :line at :file ' ,
202+ if ($ this ->isApiContext ) {
203+ $ response = $ this ->createProblemDetailsResponse (
204+ request: $ request ,
205+ status: Response::HTTP_BAD_REQUEST ,
194206 exception: $ exception ,
195- )
196- : 'Bad Request ' ;
197- $ response = new Response (content: $ message , status: Response::HTTP_BAD_REQUEST );
207+ defaultDetail: 'The request could not be understood or was missing required parameters. ' ,
208+ );
209+ } else {
210+ $ message = Environment::isDebugMode ()
211+ ? $ this ->formatExceptionMessage (
212+ template: 'An error occurred: :message at line :line at :file ' ,
213+ exception: $ exception ,
214+ )
215+ : 'Bad Request ' ;
216+ $ response = new Response (content: $ message , status: Response::HTTP_BAD_REQUEST );
217+ }
198218 } catch (Throwable $ exception ) {
199219 // Log the error for debugging
200220 error_log (sprintf (
@@ -204,33 +224,22 @@ private function handleRequest(
204224 $ exception ->getLine (),
205225 ));
206226
207- $ message = Environment::isDebugMode ()
208- ? $ this ->formatExceptionMessage (
209- template: 'Internal Server Error: :message at line :line at :file ' ,
227+ if ($ this ->isApiContext ) {
228+ $ response = $ this ->createProblemDetailsResponse (
229+ request: $ request ,
230+ status: Response::HTTP_INTERNAL_SERVER_ERROR ,
210231 exception: $ exception ,
211- )
212- : 'Internal Server Error ' ;
213-
214- // Return JSON response for API requests
215- if (str_contains (haystack: $ urlMatcher ->getContext ()->getBaseUrl (), needle: '/api ' )) {
216- $ content = Environment::isDebugMode ()
217- ? json_encode (value: [
218- 'error ' => 'Internal Server Error ' ,
219- 'message ' => $ exception ->getMessage (),
220- 'file ' => $ exception ->getFile (),
221- 'line ' => $ exception ->getLine (),
222- ])
223- : json_encode (value: ['error ' => 'Internal Server Error ' ]);
224-
225- $ response = new Response (content: $ content , status: Response::HTTP_INTERNAL_SERVER_ERROR , headers: [
226- 'Content-Type ' => 'application/json ' ,
227- ]);
228-
229- $ response ->send ();
230- return ;
232+ defaultDetail: 'An unexpected error occurred while processing your request. ' ,
233+ );
234+ } else {
235+ $ message = Environment::isDebugMode ()
236+ ? $ this ->formatExceptionMessage (
237+ template: 'Internal Server Error: :message at line :line at :file ' ,
238+ exception: $ exception ,
239+ )
240+ : 'Internal Server Error ' ;
241+ $ response = new Response (content: $ message , status: Response::HTTP_INTERNAL_SERVER_ERROR );
231242 }
232-
233- $ response = new Response (content: $ message , status: Response::HTTP_INTERNAL_SERVER_ERROR );
234243 }
235244
236245 $ response ->send ();
@@ -247,4 +256,61 @@ private function formatExceptionMessage(string $template, Throwable $exception):
247256 ':file ' => $ exception ->getFile (),
248257 ]);
249258 }
259+
260+ /**
261+ * Creates a ProblemDetails response for API errors.
262+ */
263+ private function createProblemDetailsResponse (
264+ Request $ request ,
265+ int $ status ,
266+ Throwable $ exception ,
267+ string $ defaultDetail ,
268+ ): Response {
269+ $ configuration = $ this ->container ->get (id: 'phpmyfaq.configuration ' );
270+ $ baseUrl = rtrim ($ configuration ->getDefaultUrl (), '/ ' );
271+
272+ $ type = match ($ status ) {
273+ Response::HTTP_BAD_REQUEST => $ baseUrl . '/problems/bad-request ' ,
274+ Response::HTTP_UNAUTHORIZED => $ baseUrl . '/problems/unauthorized ' ,
275+ Response::HTTP_FORBIDDEN => $ baseUrl . '/problems/forbidden ' ,
276+ Response::HTTP_NOT_FOUND => $ baseUrl . '/problems/not-found ' ,
277+ Response::HTTP_CONFLICT => $ baseUrl . '/problems/conflict ' ,
278+ Response::HTTP_UNPROCESSABLE_ENTITY => $ baseUrl . '/problems/validation-error ' ,
279+ Response::HTTP_TOO_MANY_REQUESTS => $ baseUrl . '/problems/rate-limited ' ,
280+ Response::HTTP_INTERNAL_SERVER_ERROR => $ baseUrl . '/problems/internal-server-error ' ,
281+ default => $ baseUrl . '/problems/http-error ' ,
282+ };
283+
284+ $ title = match ($ status ) {
285+ Response::HTTP_BAD_REQUEST => 'Bad Request ' ,
286+ Response::HTTP_UNAUTHORIZED => 'Unauthorized ' ,
287+ Response::HTTP_FORBIDDEN => 'Forbidden ' ,
288+ Response::HTTP_NOT_FOUND => 'Resource not found ' ,
289+ Response::HTTP_CONFLICT => 'Conflict ' ,
290+ Response::HTTP_UNPROCESSABLE_ENTITY => 'Validation failed ' ,
291+ Response::HTTP_TOO_MANY_REQUESTS => 'Too many requests ' ,
292+ Response::HTTP_INTERNAL_SERVER_ERROR => 'Internal Server Error ' ,
293+ default => 'HTTP error ' ,
294+ };
295+
296+ $ detail = Environment::isDebugMode ()
297+ ? $ exception ->getMessage () . ' at line ' . $ exception ->getLine () . ' in ' . $ exception ->getFile ()
298+ : $ defaultDetail ;
299+
300+ $ problemDetails = new ProblemDetails (
301+ type: $ type ,
302+ title: $ title ,
303+ status: $ status ,
304+ detail: $ detail ,
305+ instance: $ request ->getPathInfo (),
306+ );
307+
308+ $ response = new Response (
309+ content: json_encode ($ problemDetails ->toArray (), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ),
310+ status: $ status ,
311+ );
312+ $ response ->headers ->set ('Content-Type ' , 'application/problem+json ' );
313+
314+ return $ response ;
315+ }
250316}
0 commit comments