Version 7 of JSON-RPC

Updated 2012-01-27 23:27:28 by AMG

JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol. It uses JSON (RFC 4627) as data format, and is transport-independent. It is designed to be simple! - from the JSON-RPC specification [L1 ].

APN 20080705 A Tcl implementation of the JSON-RPC client-side API is now part of TclSOAP. Most of the server-side calls are implemented, but not integrated with any http servers.

MaxJarek JSON-RPC is my primary technology for client-server applications. I use Tk GUI on client side and Wibble as JSON-RPC http server. Sample client example:

package require JSONRPC
JSONRPC::create add -proxy "http://www.raboof.com/projects/jayrock/demo.ashx" \
        -params { val1 int val2 int }

puts [::add 2 3456]

Sample server side example (for wibble::jsonrpc::proxy package):

namespace eval ::wibble::json-rpc {
  proc add {a b} {return [expr {$a+$b}]}
}

TclSOAP don't support https transport for JSON-RPC. I have created patch for SOAP::https which is waiting in repo.

wibble::jsonrpc::proxy is my package thats change Wibble in good (for me) SOA application server. jsonrpc-proxy needs same corrects in Wibble core. I am waiting for new Wibble version then proxy package will be public available. :)

AMG: Please let me know what changes you would like in the Wibble core, and I'll see what I can incorporate directly. For anything that I can't just drop in without breaking compatibility and/or adding prerequisites, I may have ideas on how to modify the core to be more receptive to outside customization.

The next version of Wibble, when I have time to work on it, should feature charset support, rather than being tied to ISO8859-1. That's my primary goal. I'm also collecting little fixes and updates. For example, I modified [getrequest] to permit application/json-rpc to be parsed by a zone handler, and I modified [listen] to work better with TLS, both thanks to your suggestions and contributions. Another feature I'd like in the next Wibble is support for virtual hosts, by request from SDW.

MaxJarek: Ok, let's start with Wibble 0.2 :). This is initial version.

# Simple JSONRPC proxy for Wibble
# J. Lewandowski, (MaxJarek)
# Available under the Tcl/Tk license.  http://tcl.tk/software/tcltk/license.html

package provide wibble::jsonrpc::proxy 0.1

namespace eval ::wibble::zone {

proc jsonrpc-proxy {namespace state} {

  dict set state response status 200
  dict set state response header content-type "" application/json-rpc charset utf-8

  dict with state request {}
  if {$method eq "POST" && [dict exists $header content-type ""] && [dict get $header content-type ""] eq "application/json-rpc"} {

    array set json_req [json::json2dict [dehex $rawpost]]

    # execute method with params
    catch {set result [eval ${namespace}::$json_req(method) $json_req(params)]} err

  } else {

    set err "JSON-RPC error"

  }

  if {![info exists result]} {
        dict set state response content [json::write object id null \
                error [json::write object name [json::write string JSONRPCError] message [json::write string $err]]]
        ::wibble::log "JSON-RPC: $err"
  } else {
        dict set state response content [json::write object result [json::write string $result] id $json_req(id)]
  }

  sendresponse [dict get $state response]

}

}

Example Wibble startup script:

namespace eval ::wibble::json-rpc {
  proc strlen {a} {return "[string length $a]"}
  proc date {} {return [clock seconds]}
  proc add {a b} {return [expr {$a+$b}]}
}

package require wibble
package require json
package require json::write
package require wibble::jsonrpc::proxy

set ::wibble::zonehandlers {}
::wibble::handle /maxgui [list jsonrpc-proxy ::wibble::json-rpc]
::wibble::handle / notfound

wibble::listen 8080
vwait forever

Example client code:

package require JSONRPC
JSONRPC::configure -transport http
JSONRPC::create add -proxy "http://localhost:8080/maxgui" \
        -params { val1 int val2 int }
puts [::add 2 3456]

If you want any new json-rpc method simple insert new funkcion in ::wibble::json-rpc namespace

AMG: I don't see any core modifications here.

One change I made in my development version of Wibble is to add a [dict getnull] command (see [dict get] for the implementation), which simplifies this:

[dict exists $header content-type ""] && [dict get $header content-type ""] eq "application/json-rpc"

to be this:

[dict getnull $header content-type ""] eq "application/json-rpc"

This piece of code:

namespace eval ::wibble::json-rpc {
  proc strlen {a} {return "[string length $a]"}
  proc date {} {return [clock seconds]}
  proc add {a b} {return [expr {$a+$b}]}
}

can also be written:

namespace eval ::wibble::json-rpc {
  proc strlen {a} {string length $a}
  proc date {} {clock seconds}
  proc add {a b} {expr {$a + $b}}
}

It's mostly a question of style.

In your code above, I went ahead and modified the line:

  dict set state response header content-type "" "application/json-rpc; charset=utf-8"

to instead read:

  dict set state response header content-type "" application/json-rpc charset utf-8

This is how header dicts are intended to work. The empty string key maps to the value, and the other keys map to the parameters. This way, Wibble is able to read, interpret, and modify the headers without having to run the HTTP encode and decode routines.

Sadly, setting the charset here doesn't actually work, since Wibble will still convert your content to ISO8859-1. If the content consists solely of 7-bit ASCII, this has no effect, since 7-bit characters are encoded identically across ISO885-1, ASCII, and UTF-8. But this will corrupt anything else. For now, you're better off omitting the charset, or explicitly setting it to "iso8859-1".

Like I said, I intend to fix this in the next release of Wibble, though I'm still trying to figure out how it should work. Here's what I'm thinking, for now anyway.

  • Wibble will have a table mapping between HTTP charset names and Tcl encoding names. When parsing the client's accept-charset header, Wibble will ignore charsets not present in its table. If nothing remains, or if there was no accept-charset to begin with, Wibble will fall back on ISO8859-1.
  • If there's no charset in the content-type header, or the content-type header is missing altogether, Wibble assumes content is a native Tcl string, or that contentchan already has its encoding configured such that [read]ing it will produce a native Tcl string. (Not sure how to handle contentfile.) Wibble will then pick the client's favorite charset (according to the accept-charset header), configure the socket to encode using that charset, then write the content to it.
  • If there is a charset in the content-type header, Wibble assumes content or contentfile is encoded using that charset, or that contentchan is configured without encoding translation and will produce text in that charset when [read]. If that charset is among those advertised with nonzero qvalue in the client's accept-charset header, Wibble will turn off encoding translation for the socket before writing out the content. Otherwise, Wibble will decode it to a native Tcl string then perform as in the above case. By the way, it's an error for to specify an unsupported charset in the content-type header.

Form submission remains a problem [L2 ] [L3 ] [L4 ]. The only approach that makes sense, given the current state of popular web browsers, is to require that all HTML <form>s specify accept-charset="utf-8". This way the browsers will send UTF-8, a known encoding that supports every character. Without an explicit accept-charset, browser behavior varies wildly, and in no case is it possible to reliably autodetect the encoding, not without magical knowledge unavailable to Wibble (e.g. the encoding of the page from which the <form> was submitted) or ridiculous amounts of effort (e.g. statistical analysis of the byte patterns compared with typical frequencies for various encodings). Sadly, I couldn't find any configurations in which the browser actually bothered to tell the server what encoding it was using.