Search code examples
rakucro

Is there a way to consume request.body multiple times in Cro?


I'm writing a middleware that consumes request.body and does some validation like so:

before-matched {
    request-body -> (:$email, :$captcha-token, :$captcha-solution, *%_) {
        # Validate the email.
        unless Email::Valid.mx($email).so {
            response.status = 400;
            content 'application/json', %(message => 'Invalid Email');
        }

        # Validate captcha.
        unless $captcha.validate($captcha-token, $captcha-solution) {
            response.status = 401;
            content 'application/json', %(message => 'Invalid Captcha');
        }
    }
}

post -> 'api', 'subscribe' {
    put "outside";
    request-body -> (:$name, :$email, *%_) {
        put "inside";
        dd $name, $email;
        content 'application/json', %(message => $name);
    }
}

I tried consuming request.body multiple times and the connection hangs. "inside" is never printed (from the example above).

Here is a reproducible example:

use Cro::HTTP::Server;
use Cro::HTTP::Router;

sub MAIN() {
    my Cro::Service $http = Cro::HTTP::Server.new(
        http => <1.1>,
        host => "127.0.0.1",
        port => 10000,
        application => routes()
    );
    $http.start;

    put "Listening at http://127.0.0.1:10000";
    react {
        whenever signal(SIGINT) {
            say "Shutting down...";
            $http.stop;
            done;
        }
    }
}

sub routes() {
    route {
        before-matched {
            request-body-text -> $body {
                put "in before-matched: `{$body}'";
            }
        }

        post -> {
            put "in post route before request-body-text";
            dd request.body-text;
            request-body-text -> $body {
                put "in post route: `{$body}'";
            }
        }
    }
}

When making a request to this server with curl -v 'http://127.0.0.1:10000' --data-raw 'some-text' it hangs after printing these lines:

andinus@cadmium /tmp> raku cro-question-mre.raku
Listening at http://127.0.0.1:10000
in before-matched: `some-text'
in post route before request-body-text
Promise.new(scheduler => ThreadPoolScheduler.new(uncaught_handler => Callable), status => PromiseStatus::Planned)

request.body-text does return a promise, I'm not sure I understand what is happening after that. I tried using this but consuming request.body once has the same behavior. Am I doing this wrong?


Solution

  • If wanting to both consume the request body in middleware and make it available for the standard request handler, then the middleware needs to reinstate it by calling set-body. A working example can be found in the Cro OpenAPI request validator middleware.

    For your example, the change would be:

                request-body-text -> $body {
                    put "in before-matched: `{$body}'";
                    request.set-body($body);
                }
    

    The addition of the set-body call results in the desired output:

    in before-matched: `this is raw data'
    in post route before request-body-text
    Promise.new(scheduler => ThreadPoolScheduler.new(uncaught_handler => Callable), status => PromiseStatus::Planned)
    in post route: `this is raw data'
    

    A peek-body and similar has been proposed to ease writing middleware that wishes to inspect the body, but hasn't been implemented yet.