Apitte skeleton – nelogguje errory a jen zobrazi error 500

Majksa
Člen | 17
+
+1
-

Ahoj,

začal jsem používat apitte-skeleton jako backend pro mojí aplikaci a neloggují se mi errory. Nejspíš to bude nějaká chyba v Middlewares, ale nenapadá mě co. Posílám moje soubory, ale většinou to je podobné apitte-skeletonu. Jestli je potřeba ještě něco, rád pošlu.

apitte.neon

extensions:
    middlewares: Contributte\Middlewares\DI\MiddlewaresExtension
    resource: Contributte\DI\Extension\ResourceExtension
    api: Apitte\Core\DI\ApiExtension

resource:
    resources:
        App\Module\Api\:
            paths:
                - %appDir%/Module/Api
            decorator:
                inject: true
        App\Module\Backend\:
            paths:
                - %appDir%/Module/Backend
            decorator:
                inject: true

api:
    debug: false
    plugins:
        Apitte\Core\DI\Plugin\CoreSchemaPlugin:
        Apitte\Core\DI\Plugin\CoreServicesPlugin:
        Apitte\OpenApi\DI\OpenApiPlugin:
        Apitte\Debug\DI\DebugPlugin:
        Apitte\Middlewares\DI\MiddlewaresPlugin:
            tracy: false
            autobasepath: false

services:
    middleware.tryCatch:
        create: Contributte\Middlewares\TryCatchMiddleware
        tags: [middleware: [priority: 1]]
        setup:
            - setDebugMode(%debugMode%)
            - setCatchExceptions(%productionMode%) # used in debug only
    middlewares.logging:
        create: Contributte\Middlewares\LoggingMiddleware
        arguments: [@monolog.logger.default]
        tags: [middleware: [priority: 100]]
    middleware.methodOverride:
        create: Contributte\Middlewares\MethodOverrideMiddleware
        tags: [middleware: [priority: 150]]
    middleware.authenticator:
        create: App\Model\Api\Middleware\AuthenticationMiddleware(
            App\Model\Api\Security\TokenAuthenticator(%api.bot.token%)
        )
        tags: [middleware: [priority: 200]]

    api.core.dispatcher: App\Model\Api\Dispatcher\JsonDispatcher

contributte.neon

extensions:
	console: Contributte\Console\DI\ConsoleExtension(%consoleMode%)
	monolog: Contributte\Monolog\DI\MonologExtension

console:
	url: http://localhost/
	lazy: true

monolog:
	holder:
		enabled: true

	channel:
		default:
			handlers:
				- Monolog\Handler\RotatingFileHandler(%logDir%/syslog.log, 30, Monolog\Logger::WARNING)

			processors:
				- Monolog\Processor\WebProcessor()
				- Monolog\Processor\IntrospectionProcessor()
				- Monolog\Processor\MemoryPeakUsageProcessor()
				- Monolog\Processor\ProcessIdProcessor()

JsonDispatcher

<?php

declare(strict_types=1);

namespace App\Model\Api\Dispatcher;

use Apitte\Core\Dispatcher\JsonDispatcher as ApitteJsonDispatcher;
use Apitte\Core\Handler\IHandler;
use Apitte\Core\Http\ApiRequest;
use Apitte\Core\Http\ApiResponse;
use Apitte\Core\Http\RequestAttributes;
use Apitte\Core\Router\IRouter;
use Apitte\Core\Schema\Endpoint;
use Nette\Utils\Json;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class JsonDispatcher extends ApitteJsonDispatcher
{
    /** @var SerializerInterface */
    protected SerializerInterface $serializer;

    /** @var ValidatorInterface */
    protected ValidatorInterface $validator;

    public function __construct(
        IRouter $router,
        IHandler $handler,
        SerializerInterface $serializer,
        ValidatorInterface $validator
    ) {
        parent::__construct($router, $handler);

        $this->serializer = $serializer;
        $this->validator = $validator;
    }

    protected function handle(ApiRequest $request, ApiResponse $response): ApiResponse
    {
        try {
            $request = $this->transformRequest($request);
            $result = $this->handler->handle($request, $response);

            // Except ResponseInterface convert all to json
            if (!($result instanceof ApiResponse)) {
                $response = $this->transformResponse($result, $response);
            } else {
                $response = $result;
            }
        } catch (\Apitte\Core\Exception\Api\ClientErrorException | \Apitte\Core\Exception\Api\ServerErrorException $e) {
            $data = [];

            if ($e->getMessage()) {
                $data['message'] = $e->getMessage();
            }
            if ($e->getContext()) {
                $data['context'] = $e->getContext();
            }
            if ($e->getCode()) {
                $data['code'] = $e->getCode();
            }

            $response = $response->withStatus($e->getCode() ?: 500)
                ->withHeader('Content-Type', 'application/json');
            $response->getBody()->write(Json::encode($data));
        } catch (\RuntimeException $e) {
            $response = $response->withStatus($e->getCode() ?: 500)
                ->withHeader('Content-Type', 'application/json');
            $response->getBody()->write(Json::encode([
                'message' => $e->getMessage() ?: 'Application encountered an internal error. Please try again later.',
            ]));
        }

        return $response;
    }

    /**
     * Transform incoming request to request DTO, if needed.
     */
    protected function transformRequest(ApiRequest $request): ApiRequest
    {
        // If Apitte endpoint is not provided, skip transforming.
        $endpoint = $request->getAttribute(RequestAttributes::ATTR_ENDPOINT);
        if (!$endpoint) {
            return $request;
        }

        // @safety
        \assert($endpoint instanceof Endpoint);

        // Get incoming request entity class, if defined. Otherwise, skip transforming.
        $entity = $endpoint->getTag('request.dto');
        if (!$entity) {
            return $request;
        }

        try {
            // Create request DTO from incoming request, using serializer.
            $dto = $this->serializer->deserialize(
                $request->getBody()->getContents(),
                $entity,
                'json',
                ['allow_extra_attributes' => false],
            );

            $request = $request->withParsedBody($dto);
        } catch (\Symfony\Component\Serializer\Exception\ExtraAttributesException $e) {
            throw \Apitte\Core\Exception\Api\ValidationException::create()
                ->withMessage($e->getMessage());
        }

        // Try to validate entity only if its enabled
        $violations = $this->validator->validate($dto);

        if (\count($violations) > 0) {
            $fields = [];
            foreach ($violations as $violation) {
                $fields[$violation->getPropertyPath()][] = $violation->getMessage();
            }

            throw \Apitte\Core\Exception\Api\ValidationException::create()
                ->withMessage('Invalid request data')
                ->withFields($fields);
        }

        return $request;
    }

    /**
     * Transform outgoing response data to JSON, if needed.
     *
     * @param mixed $data
     */
    protected function transformResponse($data, ApiResponse $response): ApiResponse
    {
        $response = $response->withStatus(200)
            ->withHeader('Content-Type', 'application/json');

        // Serialize entity with symfony/serializer to JSON
        $serialized = $this->serializer->serialize($data, 'json');

        $response->getBody()->write($serialized);

        return $response;
    }
}

AuthenticationMiddleware

<?php

declare(strict_types=1);

namespace App\Model\Api\Middleware;

use App\Model\Utils\Strings;
use Contributte\Middlewares\IMiddleware;
use Contributte\Middlewares\Security\IAuthenticator;
use Nette\Utils\Json;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class AuthenticationMiddleware implements IMiddleware
{
    private const WHITELIST_PATHS = ['/backend/v1/user/register', '/backend/v1/user/login', '/backend/v1/openapi'];

    /** @var IAuthenticator */
    private IAuthenticator $authenticator;

    public function __construct(IAuthenticator $authenticator)
    {
        $this->authenticator = $authenticator;
    }

    protected function denied(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $response->getBody()->write(Json::encode([
            'status' => 'error',
            'message' => 'Client authentication failed',
            'code' => 401,
        ]));

        return $response
            ->withHeader('Content-Type', 'application/json')
            ->withStatus(401);
    }

    protected function isWhitelisted(ServerRequestInterface $request): bool
    {
        foreach (self::WHITELIST_PATHS as $whitelist) {
            if (Strings::startsWith($request->getUri()->getPath(), $whitelist)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Authenticate user from given request
     */
    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ): ResponseInterface {
        if ($this->isWhitelisted($request)) {
            return $next($request, $response);
        }
        // If we have a identity, then go to next middlewares,
        // otherwise stop and return current response
        if (!$this->authenticator->authenticate($request)) {
            return $this->denied($request, $response);
        }

        // Pass to next middleware
        return $next($request, $response);
    }
}
Majksa
Člen | 17
+
0
-

Jenom dodám, že ostatní logování mi normálně funguje:

log/info.log

[2020-11-15 19-30-27] Requested url: http://localhost:8000/api/  @  http://localhost:8000/api/
[2020-11-15 19-30-28] Requested url: http://localhost:8000/backend/  @  http://localhost:8000/backend/
[2020-11-15 19-30-28] Requested url: http://localhost:8000/backend/v1/user/register  @  http://localhost:8000/backend/v1/user/register

log/debug.log

[2020-11-15 20-29-34] array (2) message => "Query: "START TRANSACTION"" (26) context => array (1) |  time => "1 ms" (4)  @  http://localhost:8000/backend/v1/user/register
[2020-11-15 20-29-34] array (2) message => "Query: INSERT INTO user (username, email, password, last_logged_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)" (117) context => array (1) |  time => "3 ms" (4)  @  http://localhost:8000/backend/v1/user/register
[2020-11-15 20-29-34] array (2) message => "Query: "COMMIT"" (15) context => array (1) |  time => "1 ms" (4)  @  http://localhost:8000/backend/v1/user/register

Editoval Majksa (16. 11. 2020 12:48)

Majksa
Člen | 17
+
0
-

A ještě directory tree, jestli to pomůže

├───app
│   │   Bootstrap.php
│   │
│   ├───config
│   │   │   config.local.neon
│   │   │   config.local.neon.dist
│   │   │
│   │   ├───app
│   │   │       parameters.neon
│   │   │       services.neon
│   │   │
│   │   ├───env
│   │   │       base.neon
│   │   │       dev.neon
│   │   │       prod.neon
│   │   │       test.neon
│   │   │
│   │   └───ext
│   │           apitte.neon
│   │           contributte.neon
│   │           nettrine.neon
│   │
│   ├───Domain
│   │   └───Backend
│   │       ├───Facade
│   │       │       Users.php
│   │       │
│   │       └───Request
│   │               LoginUserReqDto.php
│   │               RegisterUserReqDto.php
│   │
│   ├───Model
│   │   │   App.php
│   │   │
│   │   ├───Api
│   │   │   │   RequestAttributes.php
│   │   │   │
│   │   │   ├───Dispatcher
│   │   │   │       JsonDispatcher.php
│   │   │   │
│   │   │   ├───Middleware
│   │   │   │       AuthenticationMiddleware.php
│   │   │   │
│   │   │   └───Security
│   │   │           AbstractAuthenticator.php
│   │   │           TokenAuthenticator.php
│   │   │
│   │   ├───Database
│   │   │   │   EntityManager.php
│   │   │   │   TRepositories.php
│   │   │   │
│   │   │   ├───Entity
│   │   │   │   │   AbstractEntity.php
│   │   │   │   │   User.php
│   │   │   │   │
│   │   │   │   └───Attributes
│   │   │   │           TCreatedAt.php
│   │   │   │           TId.php
│   │   │   │           TUpdatedAt.php
│   │   │   │
│   │   │   └───Repository
│   │   │           AbstractRepository.php
│   │   │           User.php
│   │   │
│   │   ├───Exception
│   │   │   │   LogicException.php
│   │   │   │   RuntimeException.php
│   │   │   │
│   │   │   ├───Logic
│   │   │   │       InvalidArgumentException.php
│   │   │   │
│   │   │   └───Runtime
│   │   │       │   AuthenticationException.php
│   │   │       │   InvalidStateException.php
│   │   │       │   IOException.php
│   │   │       │
│   │   │       ├───Database
│   │   │       │       EntityNotFoundException.php
│   │   │       │       PersistenceException.php
│   │   │       │
│   │   │       └───User
│   │   │               EmailTakenException.php
│   │   │               UsernameTakenException.php
│   │   │
│   │   ├───Security
│   │   │       Passwords.php
│   │   │
│   │   └───Utils
│   │           Arrays.php
│   │           DateTime.php
│   │           FileSystem.php
│   │           Html.php
│   │           Image.php
│   │           Strings.php
│   │           Validators.php
│   │
│   ├───Module
│   │   ├───Api
│   │   │   │   Base.php
│   │   │   │
│   │   │   └───V1
│   │   │           Base.php
│   │   │           OpenApi.php
│   │   │
│   │   └───Backend
│   │       │   Base.php
│   │       │
│   │       └───V1
│   │               Base.php
│   │               OpenApi.php
│   │               User.php
│   │
│   └───resources
│       └───tracy
│               500.json
│               500.phtml
│               500.txt
├───bin
│       console
├───db
│   ├───Fixtures
│   │       AbstractFixture.php
│   │
│   └───Migrations
│           Version20201115144710.php
├───log
│       .gitignore
│       debug.log
│       info.log
├───temp
├───tests
│   ├───api
│   ├───backend
│   ├───integration
│   │   └───config
│   ├───unit
│   ├───_data
│   ├───_output
│   ├───_support
│   └───_temp
├───vendor
└───www
        .htaccess
        index.php
        robots.txt
        web.config
Felix
Nette Core | 1183
+
0
-

Ahoj!

  1. Jsem rad, ze to stavis na/podle contributte/apitte-skeleton.
  2. Jenom se chci ujistit, kde by jsi ty errory chtel logovat?
  3. A co se deje, kdyz se neloguji?
  4. Ukazuje se error, bila stranka, nebo neco jineho?
  5. Zkousel jsi xdebug a krokovat si to?
  6. Muzes ten projekt pushnout nekam?
Majksa
Člen | 17
+
0
-

Ahoj :)

  1. Já taky. Díky, že to existuje!
  2. Errory bych chtěl logovat pomocí contributte/monolog, do nastavené cesty: takže nejspíš log/error.log nebo něco takového.
  3. +
  4. Děje se to například, když selže INSERT request do databáze při porušení unique. Vypíše to:
{
	"status": "error",
	"code": 500,
	"message": "Application encountered an internal error. Please try again later."
}

Ale nemám jak se dostat k problému, protože error není zalogován ani vypsán.
Mode je debug.

  1. Ještě ne.
  2. Zatím to mám na soukromém GitLab repozitáři ale můžu vytvořit nový public.
Majksa
Člen | 17
+
0
-

Odkaz na repozitář:
https://github.com/…tte-log-help

Majksa
Člen | 17
+
0
-

Jenom dodám, že například když je chyba v configu, tak to normálně ukáže Tracy Error page.

Sejber
Člen | 10
+
0
-

@Majksa podařilo se to nějak vyřešit?

vml
Člen | 2
+
0
-

Řešil jsem stejný problém a vyřešil jsem ho pomocí error dekorátoru. V dokumentaci zde

Chyby najdeš pak ve složce log, jak jsi zvyklý.

appite.neon

apitte:
	debug: %debugMode%
	catchException: true # Sets if exception should be catched and transformed into response or rethrown to output (debug only)

	plugins:
		Apitte\Console\DI\ConsolePlugin:
		Apitte\Debug\DI\DebugPlugin:
			debug:
				panel: %debugMode%
				negotiation: %debugMode%
		Apitte\Core\DI\Plugin\CoreDecoratorPlugin:

services:
	decorator.request.authentication:
		class: App\Model\Security\RequestAuthenticationDecorator
		tags:
			apitte.core.decorator:
				priority: 1
				type: handler.before
	decorator.error.exception:
		class: App\Model\Debug\ExceptionDecorator

ExceptionDecorator.php

<?php

namespace App\Model\Debug;

use Apitte\Core\Decorator\IErrorDecorator;
use Apitte\Core\Exception\Api\ClientErrorException;
use Apitte\Core\Exception\Api\ServerErrorException;
use Apitte\Core\Http\ApiRequest;
use Apitte\Core\Http\ApiResponse;
use Throwable;
use Tracy\Debugger;
use Tracy\ILogger;

class ExceptionDecorator implements IErrorDecorator
{

	public function decorateError(ApiRequest $request, ApiResponse $response, Throwable $error): ApiResponse
	{
		Debugger::log($error, ILogger::ERROR);

		if ($error instanceof ServerErrorException) {
			$response = $response->writeJsonBody([
				'error' => 'Server error.'
			])->withStatus(500);
		}
		if ($error instanceof ClientErrorException) {
			$response = $response->writeJsonBody([
				'error' => $error->getMessage(),
				'previous' => $error->getPrevious()?->getMessage(),
			])->withStatus($error->getCode());
		}
		return $response;
	}

}

Editoval vml (6. 3. 17:12)

Felix
Nette Core | 1183
+
+1
-

Pred casem nekdo zakladal issue (https://github.com/…n/issues/664) na Githubu u apitte-skeletonu.

Pridal jsem primou ukazku https://github.com/…390a76a30db1

Dej kdyztak vedet, kdyby neco.