michalsn / codeigniter-htmx

HTMX helper library for CodeIgniter 4 framework
https://michalsn.github.io/codeigniter-htmx/
MIT License
75 stars 15 forks source link

Setting response codes fails using ResponseTrait #68

Closed Kimotu closed 4 months ago

Kimotu commented 4 months ago

How do I return API-Responses? I use this htmx lib in a ResourcePresenter and want to return codes matching the action (201 => created, 202 => accepted, ...).

But when I include ResponseTrait and return $this->respondCreated(), I get

Return value must be of type string, Michalsn\CodeIgniterHtmx\HTTP\Response returned

Any hint/idea how to fix it?

michalsn commented 4 months ago

Can you provide a sample code (controller) to reproduce the problem?

Kimotu commented 4 months ago

Sure. I'll provide an example on Monday.

Kimotu commented 4 months ago

@michalsn I found the problem. My method was defined with return type string (which usually works with html or json strings as output): public function index() : string { ... }

When I change it to: public function index() { ... } it works.

Now I just have the problem that CI only seems to support Response Types for json and xml. So I would have to write a formatter for html. Or do you know an easy solution to return a html page with http type 201?

edit: I wrote a NullFormatter, that just returns the data array unchanged and added 'text/html' in Config/Format.php. Works for 201 (created), but leads to the next error when returning 400 for validation errors, since HTMX just handles 2XX codes. Maybe there is a reason not to support html in Response Traits and use them just for API calls that return json/xml anyway.

michalsn commented 4 months ago

As for the response type, you can just set $this->stringAsHtml = true, which should work fine - no need for writing a custom formatter.

If you want to handle error codes like 400 normally, you have to write some additional code:

document.addEventListener('htmx:responseError', function(event) {
    let statusCode = event.detail.xhr.status;

    let ignoredStatusCodes = [400];

    if (ignoredStatusCodes.includes(statusCode)) {
        event.preventDefault();

        // Show content normally (you can customize this part)
        // For example, replace the target element's content with the response text
        event.detail.target.innerHTML = event.detail.xhr.responseText;
    }
});

You can make it work only when specific elements trigger the request etc. https://htmx.org/events/#htmx:responseError The above code is not tested - and may require some changes.

Kimotu commented 4 months ago

Ok cool. I tried it in 4.5.0 and found $stringAsHtml = true in upgrade guide, but response is still JSON. The HTMX part to ignore 400 works.

API\ResponseTrait and String Data

In previous versions, if you pass string data to a trait method, the framework returned an HTML response, even if the response format was determined to be JSON.

Now if you pass string data, it returns a JSON response correctly. See also Handling Response Types.

You can keep the behavior in previous (shouldn it be be current?) versions if you set the $stringAsHtml property to true in your controller.

I checked the source of ReponseTrait.php

       $asHtml = $this->stringAsHtml ?? false;

        // Returns as HTML.
        if (
            ($mime === 'application/json' && $asHtml && is_string($data))
            || ($mime !== 'application/json' && is_string($data))
        ) {
            // The content type should be text/... and not application/...
            $contentType = $this->response->getHeaderLine('Content-Type');
            $contentType = str_replace('application/json', 'text/html', $contentType);
            $contentType = str_replace('application/', 'text/', $contentType);
            $this->response->setContentType($contentType);
            $this->format = 'html';

            return $data;
        }

But even, when I set $mime = application/json and $asHtml = true and verified that $data is of type string, it outputs json.

$this->stringAsHtml = true;                    // --> $asHtml = true
$this->setResponseFormat('application/json');  // --> $mine = 'application/json'
return $this->fail(gettype("Test"));           // --> gettype returns string and 'Test' is string => all conditions met

returns

{
    "status": 400,
    "error": 400,
    "messages": {
        "error": "string"
    }
}
neznaika0 commented 4 months ago

Perhaps you are talking about this? https://forum.codeigniter.com/showthread.php?tid=90589

Kimotu commented 4 months ago

Hey, good hint. But actually it seems, the problem is RespondTrait itself. All failure-methods just return JSON.

return $this->respond(view('home'),400); or $this->respondCreated(view('home')); respect $this->stringAsHtml = true; and return HTML.

return $this->fail(view('home'),400); or $this->failValidationError(view('home')); just return JSON.

Well, now that I know the stringAsHtml flag, I can use the response-methods and manually set the error codes.

Hint: I checked the source of RespondTrait and all methods finally call a generic respond()-method, but failure-methods first call a generic fail()-method that sets an error array as $data and then calls the generic respond()-method. I guess this error array forces the output to be JSON, because in format()-method the array does not match the is_string($data) check.

michalsn commented 4 months ago

You can now use HTMLFormatter - https://michalsn.github.io/codeigniter-htmx/html_formatter/