Skip to content

feat: Error handler for HTMX #92

@neznaika0

Description

@neznaika0

@michalsn Is it possible to add a handler for all HTMX exceptions?

Reason

For example, we make HTMX requests from the page and replace part of the content with body > content > htmx data. Everything is working fine.

When status code 400-500, we get nothing in Response. Yes, there is a built-in debugger in the modal window, but this is not the best option. I would like the error to be configurable.

Decision

We create a new HTMXExceptionHandler, check the Request::isHtmx() and show a custom error page. Right now I don't see the full picture of what this will affect, so I'm writing here. Personally, I would like to show a small "toast" notification when an error occurs. This will make it much easier to understand that the request failed.

Alternatively, you can show the full error page from the default handler, or the full page if the response is not going to be used as a "swap-oob", "toast" notification.

Example
To get a response 400-500 in HTMX, we need to fix the configuration (or send 200 ?).

<?php

declare(strict_types=1);

namespace Neznaika0\ErrorHandlers;

use CodeIgniter\Debug\BaseExceptionHandler;
use CodeIgniter\Debug\ExceptionHandler;
use CodeIgniter\Debug\ExceptionHandlerInterface;
use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Michalsn\CodeIgniterHtmx\HTTP\IncomingRequest;
use Michalsn\CodeIgniterHtmx\HTTP\Response;
use Throwable;

class HTMXExceptionHandler extends BaseExceptionHandler implements ExceptionHandlerInterface
{
    /**
     * {@inheritDoc}
     *
     * @param CLIRequest|IncomingRequest $request
     * @param Response|ResponseInterface $response
     */
    public function handle(
        Throwable $exception,
        RequestInterface $request,
        ResponseInterface $response,
        int $statusCode,
        int $exitCode,
    ): void {
        // Backward compatibility with regular Requests 
        if (! $request->isHtmx()) {
            $defaultHandler = new ExceptionHandler($this->config);
            $defaultHandler->handle($exception, $request, $response, $statusCode, $exitCode);

            return;
        }

        $response->setStatusCode($statusCode);

        $viewAppFile  = APPPATH . 'Views/errors/html/error_htmx.php';
       // Default view if not exist in /app/
        $viewMainFile = __DIR__ . '../Views/errors/html/error_htmx.php';

        $viewFile = is_file($viewAppFile) ? $viewAppFile : $viewMainFile;

        // Send real status code "400-500" instead "200"
        $response->sendHeaders();
        $this->render($exception, $statusCode, $viewFile);

        // Copied from original
        if (ENVIRONMENT !== 'testing') {
            // @codeCoverageIgnoreStart
            exit($exitCode);
            // @codeCoverageIgnoreEnd
        }
    }
}
    // app/Config/Exceptions.php
    public function handler(int $statusCode, Throwable $exception): ExceptionHandlerInterface
    {
        return new HTMXExceptionHandler($this);
        // return new ExceptionHandler($this);
    }

// app/Views/errors/html/error_htmx.php
<div class="htmx-exception">
    <h1><?= $code ?></h1>
    <p>
        <?php if (ENVIRONMENT !== 'production') : ?>
            <?= nl2br(esc($message)) ?>
        <?php else: ?>
            Ooops!
        <?php endif; ?>
    </p>
</div>

// JS code
document.addEventListener('htmx:beforeSwap', function (e) {
    if (e.detail.xhr.status >= 400) {
        e.detail.shouldSwap = true;
        e.detail.isError = false;
    }
});

Others

Does such a decision make sense, will it bring benefits?

There are a few thoughts for the tests:

  • HTMX request with partial replacement (in content, sidebar, toasts)
  • HTMX request with an error page opening (for example, redirection after the form)
  • The error page must have a full version with and a partial version with fragments to replace.
  • Common exceptions should be handled by the standard handler

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions