AWS-Lambda
view release on metacpan or search on metacpan
lib/AWS/Lambda/PSGI.pm view on Meta::CPAN
my $mode = $ENV{PERL5_LAMBDA_PSGI_INVOKE_MODE}
|| $ENV{AWS_LWA_INVOKE_MODE} # for compatibility with https://github.com/awslabs/aws-lambda-web-adapter
|| "BUFFERED";
$self->{invoke_mode} = uc $mode;
}
return $self;
}
sub prepare_app { return }
sub app {
return $_[0]->{app} if scalar(@_) == 1;
return $_[0]->{app} = scalar(@_) == 2 ? $_[1] : [ @_[1..$#_ ]];
}
sub to_app {
my $self = shift;
$self->prepare_app;
return sub { $self->call(@_) };
}
sub wrap {
my($self, $app, @args) = @_;
# Lambda function runs as reverse proxy backend.
# So, we always enable ReverseProxy middleware.
$app = Plack::Middleware::ReverseProxy->wrap($app);
if (ref $self) {
$self->{app} = $app;
} else {
$self = $self->new({ app => $app, @args });
}
return $self->to_app;
}
sub call {
my($self, $env, $ctx) = @_;
# $ctx is added by #26
# fall back to $AWS::Lambda::context because of backward compatibility.
$ctx ||= $AWS::Lambda::context;
if ($self->{invoke_mode} eq "RESPONSE_STREAM") {
my $input = $self->_format_input_v2($env, $ctx);
$input->{'psgi.streaming'} = Plack::Util::TRUE;
my $res = $self->app->($input);
return $self->_handle_response_stream($res);
} else {
my $input = $self->format_input($env, $ctx);
my $res = $self->app->($input);
return $self->format_output($res);
}
}
sub format_input {
my ($self, $payload, $ctx) = @_;
if (my $context = $payload->{requestContext}) {
if ($context->{elb}) {
# Application Load Balancer https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html
return $self->_format_input_v1($payload, $ctx);
}
}
if (my $version = $payload->{version}) {
if ($version =~ /^1[.]/) {
# API Gateway for REST https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
return $self->_format_input_v1($payload, $ctx);
}
if ($version =~ /^2[.]/) {
# API Gateway for HTTP https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
return $self->_format_input_v2($payload, $ctx);
}
}
return $self->_format_input_v1($payload, $ctx);
}
sub _format_input_v1 {
my ($self, $payload, $ctx) = @_;
my $env = {};
# merge queryStringParameters and multiValueQueryStringParameters
my $query = {
%{$payload->{queryStringParameters} // {}},
%{$payload->{multiValueQueryStringParameters} // {}},
};
my @params;
while (my ($key, $value) = each %$query) {
if (ref($value) eq 'ARRAY') {
for my $v (@$value) {
push @params, "$key=$v";
}
} else {
push @params, "$key=$value";
}
}
$env->{QUERY_STRING} = join '&', @params;
# merge headers and multiValueHeaders
my $headers = {
%{$payload->{headers} // {}},
%{$payload->{multiValueHeaders} // {}},
};
while (my ($key, $value) = each %$headers) {
$key =~ s/-/_/g;
$key = uc $key;
if ($key !~ /^(?:CONTENT_LENGTH|CONTENT_TYPE)$/) {
$key = "HTTP_$key";
}
if (ref $value eq "ARRAY") {
$value = join ", ", @$value;
}
$env->{$key} = $value;
}
$env->{'psgi.version'} = [1, 1];
$env->{'psgi.errors'} = *STDERR;
$env->{'psgi.run_once'} = Plack::Util::FALSE;
$env->{'psgi.multithread'} = Plack::Util::FALSE;
$env->{'psgi.multiprocess'} = Plack::Util::FALSE;
$env->{'psgi.streaming'} = Plack::Util::FALSE;
$env->{'psgi.nonblocking'} = Plack::Util::FALSE;
$env->{'psgix.harakiri'} = Plack::Util::TRUE;
$env->{'psgix.input.buffered'} = Plack::Util::TRUE;
# inject the request id that compatible with Plack::Middleware::RequestId
if ($ctx) {
$env->{'psgix.request_id'} = $ctx->aws_request_id;
$env->{'HTTP_X_REQUEST_ID'} = $ctx->aws_request_id;
}
lib/AWS/Lambda/PSGI.pm view on Meta::CPAN
}
$writer->close or die "failed to close writer: $!";
return;
};
$response->($psgi_responder);
};
}
sub _format_response_stream {
my ($self, $status, $headers) = @_;
my $headers_hash = {};
my $cookies = [];
Plack::Util::header_iter($headers, sub {
my ($k, $v) = @_;
$k = lc $k;
if ($k eq 'set-cookie') {
push @$cookies, string $v;
} elsif (exists $headers_hash->{$k}) {
$headers_hash->{$k} = ", $v";
} else {
$headers_hash->{$k} = string $v;
}
});
return +{
statusCode => number $status,
headers => $headers_hash,
cookies => $cookies,
};
}
1;
__END__
=encoding utf-8
=head1 NAME
AWS::Lambda::PSGI - It translates event of Lambda Proxy Integrations in API Gateway and
Application Load Balancer into L<PSGI>.
=head1 SYNOPSIS
Add the following script into your Lambda code archive.
use utf8;
use warnings;
use strict;
use AWS::Lambda::PSGI;
my $app = require "$ENV{'LAMBDA_TASK_ROOT'}/app.psgi";
my $func = AWS::Lambda::PSGI->wrap($app);
sub handle {
return $func->(@_);
}
1;
And then, L<Set up Lambda Proxy Integrations in API Gateway|https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html> or
L<Lambda Functions as ALB Targets|https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html>
=head1 DESCRIPTION
=head2 Streaming Response
L<AWS::Lambda::PSGI> supports L<response streaming|https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html>.
The function urls's invoke mode is configured as C<"RESPONSE_STREAM">, and Lambda environment variable "PERL5_LAMBDA_PSGI_INVOKE_MODE" is set to C<"RESPONSE_STREAM">.
ExampleApi:
Type: AWS::Serverless::Function
Properties:
FunctionUrlConfig:
AuthType: NONE
InvokeMode: RESPONSE_STREAM
Environment:
Variables:
PERL5_LAMBDA_PSGI_INVOKE_MODE: RESPONSE_STREAM
# (snip)
In this mode, the PSGI server accepts L<Delayed Response and Streaming Body|https://metacpan.org/pod/PSGI#Delayed-Response-and-Streaming-Body>.
my $app = sub {
my $env = shift;
return sub {
my $responder = shift;
$responder->([ 200, ['Content-Type' => 'text/plain'], [ "Hello World" ] ]);
};
};
An application MAY omit the third element (the body) when calling the responder.
my $app = sub {
my $env = shift;
return sub {
my $responder = shift;
my $writer = $responder->([ 200, ['Content-Type' => 'text/plain'] ]);
$writer->write("Hello World");
$writer->close;
};
};
=head2 Request ID
L<AWS::Lambda::PSGI> injects the request id that compatible with L<Plack::Middleware::RequestId>.
env->{'psgix.request_id'} # It is same value with $context->aws_request_id
=head1 LICENSE
The MIT License (MIT)
Copyright (C) ICHINOSE Shogo.
=head1 AUTHOR
ICHINOSE Shogo E<lt>shogo82148@gmail.comE<gt>
=cut
( run in 0.987 second using v1.01-cache-2.11-cpan-39bf76dae61 )