Skip Menu |

This queue is for tickets about the POE-Component-Server-JSONRPC CPAN distribution.

Report information
The Basics
Id: 31580
Status: resolved
Priority: 0/
Queue: POE-Component-Server-JSONRPC

People
Owner: typester [...] gmail.com
Requestors: FAIZ [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: Important
Broken in: 0.02
Fixed in: (no value)



Subject: JSON-RPC "id" unusable for concurrent requests
Murase-san, Thanks for having written this module. I'm using Perl 5.8.8, on Debian 4.0. The problem that I am facing with POE::Component::Server::JSONRPC is with the "id" of the JSON request and response. Sequential requests are OK, but parallel requests seem to mess up other requests' IDs: OK: (request1-response1-request2-response2 pattern) {"method":"ping", "id":"1", "params":[1,2]} # id:1 {"error":null,"id":"1","result":1} {"method":"ping", "id":"2", "params":[1,2]} # id:2 {"error":null,"id":"2","result":1} NOT OK: (request1-request2-response1-response2 pattern) {"method":"ping", "id":"1", "params":[1,2]} # id:1 {"method":"ping", "id":"2", "params":[1,2]} # id:2 {"error":null,"id":"2","result":1} # ERROR! both IDs are 2. {"error":null,"id":"2","result":1} # $ echo -e {"method":"ping", "id":"1", "params":[1,2]} \n{"method":"ping", "id":"2", "params":[1,2]}' | nc localhost 8011 (output) {"error":null,"id":"2","result":1} {"error":null,"id":"2","result":1} The problem seems to be that the "id" is stored on the process heap File: lib/POE/Component/Server/JSONRPC.pm Line: 195: $heap->{id} = $json->{id}; This means that parallel requests cannot have unique IDs, it seems.
Subject: test.pl
use strict; use warnings; use POE; use POE::Component::Server::JSONRPC; POE::Session->create( inline_states => { _start => sub { my ($kernel, $heap) = @_[KERNEL, HEAP]; $heap->{jsonrpc} = POE::Component::Server::JSONRPC->new( Port => 8011, Handler => { ping => 'ping', } ); }, ping => sub { my ($kernel, $jsonrpc) = @_[KERNEL, ARG0]; $kernel->post( $jsonrpc => result => 1); }, } ); $poe_kernel->run();
I fixed this bug, I also added HTTP support. So the module is now handling multiple clients and multiple requests well. You have to use JSONRPC::Tcp or JSONRPC::Http. I updated the doc too.
Subject: JSONRPC.pm
package POE::Component::Server::JSONRPC; use strict; use warnings; use base qw/Class::Accessor::Fast/; our $VERSION = '0.03'; use POE qw/ Filter::Line /; use JSON::Any; =head1 NAME POE::Component::Server::JSONRPC - POE tcp or http based JSON-RPC server =head1 SYNOPSIS #http version: POE::Component::Server::JSONRPC::Http->new( Port => 3000, Handler => { 'echo' => 'echo', 'sum' => 'sum', }, ); #tcp version: POE::Component::Server::JSONRPC::Tcp->new( Port => 3000, Handler => { 'echo' => 'echo', 'sum' => 'sum', }, ); sub echo { my ($kernel, $jsonrpc, @params) = @_[KERNEL, ARG0..$#_ ]; $kernel->post( $jsonrpc => 'result' => @params ); } sub sum { my ($kernel, $jsonrpc, @params) = @_[KERNEL, ARG0..$#_ ]; $kernel->post( $jsonrpc => 'result' => $params[0] + $params[1] ); } =head1 DESCRIPTION This module is a POE component for tcp or http based JSON-RPC Server. The specification is defined on http://json-rpc.org/ and this module use JSON-RPC 1.0 spec (1.1 does not cover tcp streams) =head1 METHODS =head2 new Create JSONRPC component session and return the session id. Parameters: =over =item Port Port number for listen. =item Handler Hash variable contains handler name as key, handler poe state name as value. Handler name (key) is used as JSON-RPC method name. So if you send {"method":"echo"}, this module call the poe state named "echo". =back =cut sub new { my $self = shift->SUPER::new( @_ > 1 ? {@_} : $_[0] ); $self->{parent} = $poe_kernel->get_active_session->ID; $self->{json} ||= JSON::Any->new; my $session = POE::Session->create( object_states => [ $self => { map { ( $_ => "poe_$_", ) } qw/_start init_server input_handler result error send/ }, ], ); $session->ID; } =head1 HANDLER PARAMETERS =over =item ARG0 A session id of PoCo::Server::JSONRPC itself. =item ARG1 The id of the client you're treating, send that back in result/error. =item ARG2 .. ARGN JSONRPC argguments =back ex) If you send following request {"method":"echo", "params":["foo", "bar"]} then, "echo" handler is called and parameters is that ARG0 is component session id, ARG1 is client id, ARG2 "foo", ARG3 "bar". =head1 HANDLER RESPONSE You must call either "result" or "error" state in your handlers to response result or error. ex: $kernel->post( $component_session_id => "result" => $client_id, "result value" ) $component_session_id is ARG0 in handler. If you do above, response is: {"result":"result value", "error":""} =head1 POE METHODS Inner method for POE states. =head2 poe__start =cut sub poe__start { my ($self, $kernel, $session, $heap) = @_[OBJECT, KERNEL, SESSION, HEAP]; $heap->{clients} = {}; $heap->{id} = 0; $kernel->yield('init_server'); } sub poe_init_server { print "error init_server\n"; } =head2 poe_input_handler =cut sub poe_input_handler { my ($self, $kernel, $session, $heap, $request, $response, $dirmatch) = @_[OBJECT, KERNEL, SESSION, HEAP, ARG0..$#_ ]; $heap->{clients}->{$heap->{id}} = {json_id => undef, response => $response}; my $json; eval { $json = $self->{json}->Load( $request->content ); }; if ($@) { $kernel->yield('error', $heap->{id}, q{invalid json request}); return; } $heap->{clients}->{$heap->{id}} = {json_id => $json->{id}, response => $response}; unless ($json and $json->{method}) { $kernel->yield('error', $heap->{id}, q{parameter "method" is required}); return; } unless ($self->{Handler}{ $json->{method} }) { $kernel->yield('error', $heap->{id}, qq{no such method "$json->{method}"}); return; } my $handler = $self->{Handler}{ $json->{method} }; my @params = @{ $json->{params} || [] }; $kernel->post($self->{parent}, $handler, $session->ID, $heap->{id}, @params); $heap->{id}++; if ($heap->{id}>=65535) { # limit to 2 bytes $heap->{id} = 0; } } =head2 poe_result =cut sub poe_result { my ($self, $kernel, $heap, $id, @results) = @_[OBJECT, KERNEL, HEAP, ARG0..$#_ ]; #~ print "answering to ".$id."\n"; my $client = $heap->{clients}->{$id}; my $json_content = $self->{json}->Dump( { id => $client->{json_id} || undef, error => undef, result => (@results > 1 ? \@results : $results[0]), } ); #~ print $json_content."\n"; $kernel->yield('send',$client->{response},$json_content); } =head2 poe_error =cut sub poe_error { my ($self, $kernel, $heap, $id, $error) = @_[OBJECT, KERNEL, HEAP, ARG0..$#_]; my $client = $heap->{clients}->{$id}; my $json_error_content = $self->{json}->Dump( { id => $client->{json_id} || undef, error => $error, result => undef, } ); $kernel->yield('send',$client->{response},$json_error_content); } =head2 poe_send =cut sub poe_send { print "error poe_send\n"; } =head1 AUTHOR Daisuke Murase <typester@cpan.org> Côme BERNIGAUD <come.bernigaud@laposte.net> =head1 COPYRIGHT This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. The full text of the license can be found in the LICENSE file included with this module. =cut 1;
Subject: Http.pm
package POE::Component::Server::JSONRPC::Http; use strict; use warnings; use POE::Component::Server::JSONRPC; # for old Perl 5.005 use base qw(POE::Component::Server::JSONRPC); our $VERSION = '0.01'; use POE qw/ Component::Server::SimpleHTTP Filter::Line /; use JSON::Any; use Data::Dumper; sub new { my $class = shift; return $class->SUPER::new(@_); } sub poe_init_server { my ($self, $kernel, $session, $heap) = @_[OBJECT, KERNEL, SESSION, HEAP]; $kernel->alias_set( 'JSONRPCHTTP' ); $self->{http} = POE::Component::Server::SimpleHTTP->new( 'ALIAS' => 'HTTPD', 'PORT' => $self->{Port}, $self->{Address} ? ('ADDRESS' => $self->{Address} ) : (), $self->{Hostname} ? ('HOSTNAME' => $self->{Hostname} ) : (), 'HANDLERS' => [ { 'DIR' => '.*', 'SESSION' => 'JSONRPCHTTP', 'EVENT' => 'input_handler', }, ], 'LOGHANDLER' => {}, ); } sub poe_send { my ($kernel,$response, $content) = @_[KERNEL,ARG0..$#_]; #HTTP $response->code( 200 ); $response->content( $content ); $kernel->post( 'HTTPD', 'DONE', $response ); } 1;
Subject: Tcp.pm
package POE::Component::Server::JSONRPC::Tcp; use strict; use warnings; use base qw(POE::Component::Server::JSONRPC); our $VERSION = '0.01'; use POE qw/ Component::Server::TCP Filter::Line /; use JSON::Any; sub new { my $self = shift->SUPER::new( @_ > 1 ? {@_} : $_[0] ); return $self; } sub poe_init_server { my ($self, $kernel, $session, $heap) = @_[OBJECT, KERNEL, SESSION, HEAP]; my $bind = sub { my $method = $_[0]; return sub { my ($kernel, $tcp_session, @args) = @_[KERNEL, SESSION, ARG0..$#_ ]; $kernel->post( $session->ID, $method, $tcp_session->ID, @args ); try this : $kernel->post( $session->ID, $method, {content=>$args[0]},$tcp_session->ID, "" ); }; }; $self->{tcp} = POE::Component::Server::TCP->new( Port => $self->{Port}, $self->{Address} ? ( Address => $self->{Address} ) : (), $self->{Hostname} ? ( Hostname => $self->{Hostname} ) : (), $self->{Domain} ? ( Domain => $self->{Domain} ) : (), $self->{Concurrency} ? ( Concurrency => $self->{Concurrency} ) : (), ClientInput => $bind->('input_handler'), # ClientConnected => $bind->('tcp_connect_handler'), # ClientDisconnected => $bind->('tcp_disconnect_handler'), # ClientError => $bind->('tcp_client_error_handler'), # ClientFlushed => $bind->('tcp_client_flush_handler'), ClientInputFilter => $self->{ClientInputFilter} || POE::Filter::Line->new, ClientOutputFilter => $self->{ClientOutputFilter} || POE::Filter::Line->new, InlineStates => { send => sub { my ($client, $data) = @_[ARG0..$#_]; $client->put($data) if $client; }, }, ); } sub poe_send { my ($kernel,$response, $content) = @_[KERNEL,ARG0..$#_]; # TCP $kernel->post($response => send => $response,$content); } 1;
The fix is now integrated in the module.