cloudcreativity / laravel-json-api

JSON API (jsonapi.org) package for Laravel applications.
http://laravel-json-api.readthedocs.io/en/latest/
Apache License 2.0
780 stars 109 forks source link

Endpoint not found #582

Closed ben221199 closed 3 years ago

ben221199 commented 3 years ago

In the Exception handler there is the following function:

public function render($request,Throwable $e){
    if($this->isJsonApi($request,$e)){
        return $this->renderJsonApi($request,$this->prepareException($e));
    }
    return parent::render($request,$e);
}

In this function, the code $this->isJsonApi($request,$e) detects if the current endpoint is a JSONAPI endpoint. If it is, it will render the exception in JSONAPI format.

However, when the endpoint does not exist, this $this->isJsonApi($request,$e) returns false and Laravel will show the non-JSONAPI version of a 404 page, so the page will return HTML. What do I have to do to get a JSONAPI error when the endpoint doesn't exist? I don't want to use Route::fallback.

Example:

ben221199 commented 3 years ago

I'm using a website and an API in one single Laravel project.

ben221199 commented 3 years ago

In the RouteServiceProvider I have this code:

/**
 * Define the routes for the application.
 * @return void
 */
public function map(): void{
    $this->mapApiRoutes();
    $this->mapWebRoutes();
}
/**
 * Define the "api" routes for the application.
 * These routes are typically stateless.
 * @return void
 */
protected function mapApiRoutes(): void{
    Route::middleware('api')->domain('api.mydomain.test')->name('api:')->group(base_path('routes/api.php'));
}
/**
 * Define the "web" routes for the application.
 * These routes all receive session state, CSRF protection, etc.
 * @return void
 */
protected function mapWebRoutes(): void{
    Route::middleware('web')->group(base_path('routes/web.php'));
}
lindyhopchris commented 3 years ago

Can you confirm what the Accept header is when you send the request to the endpoint that doesn't exist?

If your Accept header has the value application/vnd.api+json, you should get a JSON:API response. If it doesn't, then the HTML response is correct.

ben221199 commented 3 years ago

But why should I set an Accept header when the whole domain (api.mydomain.test) is meant to give API responses?

lindyhopchris commented 3 years ago

Because that's how HTML content negotiation works. From Mozilla:

The Accept request HTTP header advertises which content types, expressed as MIME types, the client is able to understand. Using content negotiation, the server then selects one of the proposals, uses it and informs the client of its choice with the Content-Type response header.

Technically a Laravel app out-of-the-box supports multiple content types from the exception handler: HTML and JSON. The JSON:API package effectively adds JSON:API in as a supported type - but doesn't override the fact that the application renders exceptions for HTML and regular JSON. I.e. we add an additional supported Content-Type that the exception handler can choose to return.

If you want your exception handler to only over render as JSON:API then you'd need to just set up your render method on the exception handler so it only ever returns JSON:API. (Or only ever returns it if the request URI matches your whole API domain.) I.e. implement your own logic for isJsonApi().

Does that make sense?

ben221199 commented 3 years ago

Hmmm, I could try to look for a specific namespace or guard.

lindyhopchris commented 3 years ago

You could probably check whether it's the api domain using a method on the request. There's a fullUrlIs() method that looks useful for matching the domain in the URL.

lindyhopchris commented 3 years ago

Going to close this, as I think the issue is answered - we add an additional media type supported by the exception handler, not replace them.

If a developer wants to only ever render JSON:API, then this:

  public function render($request, Throwable $e)
  {
    if ($this->isJsonApi($request, $e)) {
      return $this->renderJsonApi($request, $e);
    }

    // do standard exception rendering here...
  }

Should be replaced with this:

  public function render($request, Throwable $e)
  {
      return $this->renderJsonApi($request, $e);
  }

Or if wanting to check if it's their API they can use the request is() or fullUrlIs() methods, e.g.:

  public function render($request, Throwable $e)
  {
    if ($request->is('/api*')) {
      return $this->renderJsonApi($request, $e);
    }

    // do standard exception rendering here...
  }