core
core copied to clipboard
Cannot make GET request (Accept: text/html), it's always catched up by the documentation route
API Platform version(s) affected: 2.5.7
Description
I have a resource with only the "get item" activated. It uses a custom controller.
I want it to return either the json serialized resource or a html/twig template depending on the client Accept header/format parameter.
this doesnt work:
curl -X GET "http://127.0.0.1:9007/api/email_verifications/my_token -h "Accept: text/html"
It returns the documentation html page (but autoscrolled at the correct resource location)
How to reproduce
the resource:
<?php
namespace App\Entity\Api;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Controller\EmailVerificationController;
/**
* @ApiResource(
* formats={"html", "json", "jsonld"},
* collectionOperations={},
* itemOperations={
* "get"={
* "method"="GET",
* "requirements"={"id"=".+"},
* "controller"=EmailVerificationController::class
* }
* }
* )
*/
class EmailVerification
{
/**
* @var string
* @ApiProperty(identifier=true)
*/
public $token;
public function __construct($token)
{
$this->token = $token;
}
}
the controller :
<?php
namespace App\Controller;
use App\Entity\Api\EmailVerification;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
class EmailVerificationController
{
private $JWTEncoder;
private $em;
private $twig;
public function __construct(JWTEncoderInterface $JWTEncoder, EntityManagerInterface $em, Environment $twig)
{
$this->JWTEncoder = $JWTEncoder;
$this->em = $em;
$this->twig = $twig;
}
public function __invoke(EmailVerification $data, Request $request)
{
try {
$data = $this->JWTEncoder->decode($data->token);
} catch( \Exception $e ) {
return new Response("", 400);
}
/** @var User $user */
if( isset($data['user_id']) && ($user = $this->em->getRepository(User::class)->find($data['user_id'])) ) {
$user->setEmailVerificationRequired(false);
$this->em->flush();
if (in_array('text/html', $request->getAcceptableContentTypes())) {
return new Response($this->twig->render('transactions/email_verification.html.twig'));
}
return new Response("", 200);
} else {
return new Response("", 400);
}
}
}
Additional Context
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
patch_formats:
json: ['application/merge-patch+json']
swagger:
versions: [3]
api_keys:
apiKey:
name: Authorization
type: header
name_converter: 'Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter'
formats:
jsonld: ['application/ld+json']
json: ['application/json']
csv: ['text/csv']
html: ['text/html']
shouldn't you add a path to the item operation? Does it show up on bin/console debug:router?
Yes it shows up.
this works fine :
curl -X GET "http://127.0.0.1:9007/api/email_verifications/my_token -h "Accept: application/json"
but this returns the swagger html documentation page :
curl -X GET "http://127.0.0.1:9007/api/email_verifications/my_token -h "Accept: text/html"
This is in purpose. Disabling API Platform native support for Swagger UI support will "fix" this. Then you can re-add manually the /docs route in your own routing configuration.
Maybe should we provide another option to disable only the listener but not the /docs route.
Is there any news on this? Or some kind of workaround? :)
api_platform:
mapping:
paths:
- '%kernel.project_dir%/src/Entity'
- '%kernel.project_dir%/config/api_platform'
patch_formats:
json: ['application/merge-patch+json']
formats:
jsonld: ['application/ld+json']
json: ['application/json']
html: ['text/html']
swagger:
versions: [3]
api_keys:
JWT:
name: authorization
type: header
path_segment_name_generator: api_platform.path_segment_name_generator.dash
defaults:
formats: ['jsonld', 'json']
At this time "defaults" section resolves this issue for me.
other simple solution:
decorate the SwaggerUiListener
#[AsDecorator(decorates: 'api_platform.swagger.listener.ui')]
final class SwaggerUiListener
{
public const DISABLE_SWAGGER = 'disable_swagger';
public function __construct(
#[AutowireDecorated]
private \ApiPlatform\Symfony\Bundle\EventListener\SwaggerUiListener $decorated
) {
}
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
if (!$request->attributes->get(self::DISABLE_SWAGGER, false)) {
$this->decorated->onKernelRequest($event);
}
}
}
and add variable tu the operation
new Metadata\Get(
defaults: [SwaggerUiListener::DISABLE_SWAGGER => true],
In version 3.3 there is not SwaggerUiListener, and there is a provider. You can avoid problems not using 'html' as a format. For example, i choose 'htm' as a replace:
new Get(
name: 'email_html',
uriTemplate: '/download/{uuid}/html',
formats: ['htm' => ['text/html']], // if we use 'html', the swagger UI provider will catch up this request
......
Indeed, not a huge fan but this allows to redirect http errors to the swagger ui when enabled, we can probably add a flag to disable this which would fix this use case?
For now you can decorate this: https://github.com/api-platform/core/blob/main/src/Symfony/Bundle/SwaggerUi/SwaggerUiProvider.php
and just skip when you want.
I meet exactly the same problem (trying to enable newly registered user by clicking a link in a email)... @soyuka Could you please elaborate on Swagger UI decoration ? I made a try, but it failed. I'm quite new to service decoration, so I may have done something wrong... Could you please have a look on this commit ?
Well, it seems that my decorator works as I can see dumps in the Symfony profiler. But I continue to get 404 from Swagger when calling the url from the browser. Do I do it well here https://github.com/taophp/testapi/blob/swagger-decorator/api/src/Swagger/SwaggerUiProvider.php#L21 @soyuka ?
Mhh this is harder then I thought @taophp you should overwrite the service instead, this service is part of a decoration chain therefore if you don't call the chain you may end up with issues. As this is asked a lot maybe that we can introduce an option to skip the provider? I'll try to reproduce this.
@soyuka Yes, it would be great to have an option to skip the provider. Please, add it and let us know.
#6449 introduces the _api_disable_swagger_provider flag inside extraProperties. Will be available in the next release.
Great ! Thank you, @soyuka !
Thanks for this @soyuka !
Please, could you elaborate on how to use this new _api_disable_swagger_provider ? I give it a try here, but it does not work. Consider it in relation with this class.
@dunglas I was just stumbling over this issue and since you mentioned this was on purpose, I just want to point out, that this behaviour can be used to bypass the access restriction for non-public docs.
However, it may sound like an edge case, but we just had this in our application: If the /docs-Route is behind a Symfony firewall and some ApiResource with PUBLIC_ACCESS allows the html-Format (on purpose or by misconfiguration or by improper defaults), accessing the resource will result in access to the Docs meant to be restricted.