Search code examples
gojson-rpc

jsonrpc server to accept requested lowercase method names (intended for uppercase registered services)


I'm trying to write a jsonrpc server that will accept requested method names in lower case, eg Arith.multiply, and correctly route them to the corresponding uppercase methed, e.g Arith.Multiply. Is this possible?

P.S. It's lightweight clone of production server for testing, the API is fixed, including the lowercase method names, so I can't change requested method names to uppercase.

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"
    "github.com/gorilla/rpc"
    "github.com/gorilla/rpc/json"
)

type Args struct {
    A, B int
}

type Arith int

type Result int

func (t *Arith) Multiply(r *http.Request, args *Args, result *Result) error {
    log.Printf("Multiplying %d with %d\n", args.A, args.B)
    *result = Result(args.A * args.B)
    return nil
}

func main() {
    s := rpc.NewServer()
    s.RegisterCodec(json.NewCodec(), "application/json")
    s.RegisterCodec(json.NewCodec(), "application/json;charset=UTF-8")
    arith := new(Arith)
    s.RegisterService(arith, "")

    r := mux.NewRouter()
    r.Handle("/rpc", s)
    http.ListenAndServe(":1234", r)
}


Solution

  • It appears you can sneak something into a custom Codec to direct the lowercase method to the correct uppercase one. Compose a CodecRequest of the gorilla/rpc/json implementation and you can continue to use all of the gorilla machinery to process the request.

    Working example below. It looks long but it's all comments.

    package main
    
    import (
        "fmt"
        "log"
        "net/http"
        "strings"
        "unicode"
        "unicode/utf8"
    
        "github.com/gorilla/mux"
        "github.com/gorilla/rpc"
        "github.com/gorilla/rpc/json"
    )
    
    type Args struct {
        A, B int
    }
    type Arith int
    
    type Result int
    
    func (t *Arith) Multiply(r *http.Request, args *Args, result *Result) error {
        log.Printf("Multiplying %d with %d\n", args.A, args.B)
        *result = Result(args.A * args.B)
        return nil
    }
    
    // UpCodec creates a CodecRequest to process each request.
    type UpCodec struct {
    }
    
    // NewUpCodec returns a new UpCodec.
    func NewUpCodec() *UpCodec {
        return &UpCodec{}
    }
    
    // NewRequest returns a new CodecRequest of type UpCodecRequest.
    func (c *UpCodec) NewRequest(r *http.Request) rpc.CodecRequest {
        outerCR := &UpCodecRequest{}   // Our custom CR
        jsonC := json.NewCodec()       // json Codec to create json CR
        innerCR := jsonC.NewRequest(r) // create the json CR, sort of.
    
        // NOTE - innerCR is of the interface type rpc.CodecRequest.
        // Because innerCR is of the rpc.CR interface type, we need a
        // type assertion in order to assign it to our struct field's type.
        // We defined the source of the interface implementation here, so
        // we can be confident that innerCR will be of the correct underlying type
        outerCR.CodecRequest = innerCR.(*json.CodecRequest)
        return outerCR
    }
    
    // UpCodecRequest decodes and encodes a single request. UpCodecRequest
    // implements gorilla/rpc.CodecRequest interface primarily by embedding
    // the CodecRequest from gorilla/rpc/json. By selectively adding
    // CodecRequest methods to UpCodecRequest, we can modify that behaviour
    // while maintaining all the other remaining CodecRequest methods from
    // gorilla's rpc/json implementation
    type UpCodecRequest struct {
        *json.CodecRequest
    }
    
    // Method returns the decoded method as a string of the form "Service.Method"
    // after checking for, and correcting a lowercase method name
    // By being of lower depth in the struct , Method will replace the implementation
    // of Method() on the embedded CodecRequest. Because the request data is part
    // of the embedded json.CodecRequest, and unexported, we have to get the
    // requested method name via the embedded CR's own method Method().
    // Essentially, this just intercepts the return value from the embedded
    // gorilla/rpc/json.CodecRequest.Method(), checks/modifies it, and passes it
    // on to the calling rpc server.
    func (c *UpCodecRequest) Method() (string, error) {
        m, err := c.CodecRequest.Method()
        if len(m) > 1 && err == nil {
            parts := strings.Split(m, ".")
            service, method := parts[0], parts[1]
            r, n := utf8.DecodeRuneInString(method) // get the first rune, and it's length
            if unicode.IsLower(r) {
                upMethod := service + "." + string(unicode.ToUpper(r)) + method[n:]
                log.Printf("lowercase method %s requested: treated as %s\n", m, upMethod)
                return upMethod, err
            }
        }
        return m, err
    }
    
    func main() {
        s := rpc.NewServer()
    
        // Register our own Codec
        s.RegisterCodec(NewUpCodec(), "application/json")
        s.RegisterCodec(NewUpCodec(), "application/json;charset=UTF-8")
    
        arith := new(Arith)
        s.RegisterService(arith, "")
        r := mux.NewRouter()
        r.Handle("/rpc", s)
        fmt.Println(http.ListenAndServe(":1234", r))
    }
    

    Call the method via:

    curl -X POST -H "Content-Type: application/json" -d '{"id": 1, "method": "Arith.multiply", "params": [{"A": 10, "B": 30}]}' 127.0.0.1:1234/rpc