The easiest solution these days is probably to use [HTTP::Request::AsCGI].

This is a simple, self-contained web server. It listens on port 32090 and serves a small form. It reports the contents of the form after it has been submitted.

#!/usr/bin/perl
# Create CGI requests from HTTP::Requests, specifically the sort of
# requests that come from POE::Component::Server::HTTP.
use warnings;
use strict;
use POE;
use POE::Component::Server::HTTP;
use CGI ":standard";

# Start an HTTP server.  Run it until it's done, typically forever,
# and then exit the program.
POE::Component::Server::HTTP->new(
  Port           => 32090,
  ContentHandler => {
    '/'      => \&root_handler,
    '/post/' => \&post_handler,
  }
);
POE::Kernel->run();
exit 0;

# Handle root-level requests.  Populate the HTTP response with a CGI
# form.
sub root_handler {
  my ($request, $response) = @_;
  $response->code(RC_OK);
  $response->content(
    start_html("Sample Form")
      . start_form(
      -method  => "post",
      -action  => "/post/",
      -enctype => "application/x-www-form-urlencoded",
      )
      . "Foo: "
      . textfield("foo")
      . br() . "Bar: "
      . popup_menu(
      -name   => "bar",
      -values => [1, 2, 3, 4, 5],
      -labels => {
        1 => 'one',
        2 => 'two',
        3 => 'three',
        4 => 'four',
        5 => 'five'
      }
      )
      . br()
      . submit("submit", "submit")
      . end_form()
      . end_html()
  );
  return RC_OK;
}

# Handle simple CGI parameters.
#
# This code was contributed by Andrew Chen.  It handles GET and POST,
# but it does not handle %ENV-based CGI things.  It does not handle
# cookies, for instance.  Neither does it handle file uploads.
sub post_handler {
  my ($request, $response) = @_;

  # This code creates a CGI query.
  my $q;
  if ($request->method() eq 'POST') {
    $q = new CGI($request->content);
  }
  else {
    $request->uri() =~ /\?(.+$)/;
    if (defined($1)) {
      $q = new CGI($1);
    }
    else {
      $q = new CGI;
    }
  }

  # The rest of this handler displays the values encapsulated by the
  # object.
  $response->code(RC_OK);
  $response->content(start_html("Posted Values") 
      . "Foo = "
      . $q->param("foo")
      . br()
      . "Bar = "
      . $q->param("bar")
      . end_html());
  return RC_OK;
}

Another idea is to setup the $ENV variables yourself.

# note I've only tested GET and POST requests
# ... I haven't tried multi-part formdata yet.
  sub new_cgi {
    my $request = shift;
    local %ENV;
    require Sys::Hostname;
    require URI;
    require IO::Scalar;
    require CGI;

    #server global
    $ENV{SERVER_SOFTWARE}   = __PACKAGE__;
    $ENV{SERVER_NAME}       = Sys::Hostname::hostname;
    $ENV{GATEWAY_INTERFACE} = 'CGI/1.1';
    my $uri = URI->new($request->uri);

    #request specific
    $ENV{SERVER_PROTOCOL} = $request->server if defined $request->server;
    $ENV{SERVER_PORT}     = '32090';
    $ENV{REQUEST_METHOD}  = $request->method;
    $ENV{PATH_INFO}       = $uri->path;
    $ENV{PATH_TRANSLATED} = $uri->path;
    $ENV{SCRIPT_NAME}     = $uri->path;
    $ENV{QUERY_STRING}    = $uri->query if ($request->method eq 'GET');

    #remote host data
    #there's nothing obvious on how to get
    #this data in the HTTP::Request docs
    #$ENV{REMOTE_HOST} = undef;
    #$ENV{REMOTE_ADDR} = undef;
    #$ENV{AUTH_TYPE} = undef;
    #$ENV{REMOTE_USER} = undef;
    #$ENV{REMOTE_IDENT} = undef;
    #these are the biggies
    $ENV{CONTENT_TYPE} = join('; ', $request->content_type) || '';
    $ENV{CONTENT_LENGTH} = $request->content_length;

    # tie the content to STDIN ... this is the secret sauce of the CGI Spec
    our $content = $request->content;
    tie *STDIN, 'IO::Scalar', \$content;
    $ENV{COOKIE} = $request->header('Cookie') if $request->header('Cookie');
    return CGI->new();
  }