contributte / apitte

:wrench: An opinionated and enjoyable API framework based on Nette Framework. Supporting content negotiation, debugging, middlewares, attributes, annotations and loving openapi/swagger.
https://contributte.org/packages/contributte/apitte/
MIT License
61 stars 37 forks source link

Issues with Middleware/CORS #212

Open matronator opened 4 months ago

matronator commented 4 months ago

I've been banging my head on this for couple of hours now. I just can't seem to get CORS to work correctly. No matter what I do, no CORS headers are sent, no matter the method or URI.

Can someone please tell me what I'm missing?

config.neon (only the relevant stuff)

extensions:
    nettrine.annotations: Nettrine\Annotations\DI\AnnotationsExtension
    nettrine.cache: Nettrine\Cache\DI\CacheExtension
    middlewares: Contributte\Middlewares\DI\MiddlewaresExtension
    resource: Contributte\DI\Extension\ResourceExtension
    api: Apitte\Core\DI\ApiExtension

services:
    - App\Services\ParserService(%tplDir%)
    - App\Services\LoggerService
    # decorator.request.authentication:
    #     class: App\Api\Decorator\ExampleResponseDecorator
    #     tags: [apitte.core.decorator: [priority: 50]]
    middleware.tryCatch:
        factory: Contributte\Middlewares\TryCatchMiddleware
        tags: [middleware: [priority: 1]]
        setup:
            - setDebugMode(%debugMode%)
            - setCatchExceptions(%productionMode%) # used in debug only
            - setLogger(App\Services\LoggerService, 'info')
    middleware.logging:
        create: Contributte\Middlewares\LoggingMiddleware
        arguments: [App\Services\LoggerService]
        tags: [middleware: [priority: 100]]
    middleware.methodOverride:
        factory: Contributte\Middlewares\MethodOverrideMiddleware
        tags: [middleware: [priority: 150]]
    middleware.cors:
        factory: App\Api\Middleware\CORSMiddleware
        tags: [middleware: [priority: 200]]

    api.core.errorHandler: App\Model\ErrorHandler\ThrowingErrorHandler

api:
    debug: %debugMode%
    catchException: true
    plugins:
        Apitte\Core\DI\Plugin\CoreSchemaPlugin:
        Apitte\Core\DI\Plugin\CoreMappingPlugin:
            request:
                validator: Apitte\Core\Mapping\Validator\SymfonyValidator()
        Apitte\Core\DI\Plugin\CoreServicesPlugin:
        Apitte\Core\DI\Plugin\CoreDecoratorPlugin:
        Apitte\Debug\DI\DebugPlugin:
        Apitte\Middlewares\DI\MiddlewaresPlugin:
            tracy: true
            autobasepath: true
        Apitte\OpenApi\DI\OpenApiPlugin:
            swaggerUi:
                panel: %debugMode%
                url: null
                expansion: list # list|full|none
                filter: true # true|false|string
                title: Stacks Token Factory API

App/Api/Middleware/CORSMiddleware.php (I used the same as in apitte-skeleton, this version is only after trying to get it to work by trial and error)

<?php

declare(strict_types = 1);

namespace App\Api\Middleware;

use Contributte\Middlewares\IMiddleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class CORSMiddleware implements IMiddleware
{
    private function decorate(ResponseInterface $response): ResponseInterface
    {
        return $response
            ->withHeader('Access-Control-Allow-Origin', 'http://localhost:5173, http://localhost:3000, http://localhost:8000')
            ->withHeader('Access-Control-Allow-Credentials', 'true')
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH')
            ->withHeader('Access-Control-Allow-Headers', '*');
    }

    /**
     * Add CORS headers
     */
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface
    {
        if ($request->getMethod() === 'OPTIONS') {
            return $this->decorate($response);
        }

        /** @var ResponseInterface $response */
        $response = $next($request, $this->decorate($response));

        return $this->decorate($response);
    }
}

Controller.php

#[Path('/convert/{type}')]
#[Method(['POST', 'OPTIONS'])]
#[RequestBody('Entity', Entity::class, true, true)]
#[RequestParameter('type', 'string')]
public function convert(ApiRequest $request, ApiResponse $response): ApiResponse
{
    if ($request->getMethod() === 'OPTIONS') {
        return $response
            ->withHeader('Access-Control-Allow-Origin', '*')
            ->withHeader('Access-Control-Allow-Credentials', 'true')
            ->withHeader('Access-Control-Allow-Methods', '*')
            ->withHeader('Access-Control-Allow-Headers', '*')
            ->withStatus(200);
        // exit;
    }

    /** @var Template $template */
    $template = $request->getEntity();
    $type = $request->getParameter('type');

    $content = $this->parserService->getTemplate($this->parserService->getFilePath($this->parserService->getFilenameFromType($type)));
    $parsed = $this->parserService->parse($content, (array) $template->arguments);

    $response = $response->writeBody($parsed)->withHeader('Content-Type', 'text/plain');

    return $response;
}

But I still get no additional headers added to my response and Chrome still complains:

Snímek obrazovky 2024-05-27 v 2 16 31

However what I don't understand is why Chrome is complaining about no CORS headers, when the preflight check passed apparently, because the only request showing in the console is a POST request and not OPTIONS.

Request:

POST /api/template/convert/token HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: cs-CZ,cs;q=0.9,en;q=0.8
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 212
Content-Type: application/json
Host: localhost:8000
Origin: http://localhost:5173
Pragma: no-cache
Referer: http://localhost:5173/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
sec-ch-ua: "Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"

Response:

HTTP/1.1 500 Internal Server Error
Host: localhost:8000
Date: Mon, 27 May 2024 00:09:13 GMT
Connection: close
X-Powered-By: Nette Framework 3
X-Frame-Options: SAMEORIGIN
Set-Cookie: _nss=1; path=/; HttpOnly; SameSite=Strict
Content-Type: text/html; charset=UTF-8

But then why is the response a 500 error, that on further inspection the code shouldn't even reach that far. The actual error is a missing property on the entity (tokenSupply), which is BS, because it is sent in the payload:

{"name":"yddd","editableUri":true,"userWallet":"SP39DTEJFPPWA3295HEE5NXYGMM7GJ8MA0TQX379","tokenName":"yddd","tokenSymbol":"WDD","tokenSupply":10000,"tokenDecimals":18,"tokenURI":"","mintable":false,"burnable":false}

I am thoroughly lost...

Also, WHY ARE THERE NO HEADERS IN THE RESPONSE when I am decorating everything in the CORSMiddleware?

f3l1x commented 3 months ago

Hi @matronator, it's a complex bug report, thank you for that. Unfortunately I think your code is OK. I need from you to setup https://github.com/contributte/apitte-skeleton/ or pack your project (remove non-required stuff) and send it to me for further inspection. What's your choice?

f3l1x commented 2 months ago

ping @matronator