Skip to content

Proposed new routing format: /package/service/method #55

@spenczar

Description

@spenczar

I think we could improve Twirp's routing scheme. It has two problems right now: the /twirp/ prefix, and the package.service scheme (instead of package/service).

Some people have mentioned to me that they're hesitant to use Twirp because the routes are prefixed with /twirp, which is concerning to them for several reasons:

  • Trademarks: some very large organizations don't want to take any legal risks and are concerned that "twirp" could become trademarked.
  • Feels like advertising: This was mentioned in Add support for custom route prefixes #48: putting "twirp" in all your routes feels like it's just supposed to pump Twirp's brand.
  • Homophonous with "twerp": In some Very Serious settings (like government websites), it's not okay that "twirp" sounds like "twerp", which means something like "insignificant pest."

We currently have the /twirp prefix to avoid colliding with gRPC's routes, which live at /package.service/method, and to reserve a space for future APIs (like a reflection server).

Slashes-based routes (instead of dot-based) would be preferable because lots of existing technology expects slash-based routing. Load balancers and proxies use slashes for namespaces. If Twirp used a slash after the package name, users could leverage this much more easily. This is often possible with dots, just annoying, as you have to write a (possibly pretty complex) regex.

I think we can fix both of these in one go. This would be a breaking change at the protocol level, so it's something to take very seriously, but I think we can do it without much pain. It would just take a version bump to v6.


Proposed Implementation:

I propose we use a new routing scheme: /<package>/<service>/<method>.

This does not collide with gRPC's routes, which is good. I think it gives us flexibility to put future routes in by putting them under a Twirp package. We could provide a reflection service like this one, which lists available services:

syntax = "proto3";

package twirp.meta

import "github.com/golang/protobuf/protoc-gen-go/descriptor/descriptor.proto"

service Reflector {
  rpc ListServices(ListServiceRequest) returns (ListServiceResponse);
}

message ListServiceRequest {}
message ListServiceResponse {
  repeated protobuf.ServiceDescriptorProto services = 1;
}

It would live at /twirp.meta/Reflector, which seems good to me.

Regarding compatibility: generated clients would always use the new routing scheme. Generated servers would be able to use both, in case your users have old generated clients still around. We would accomplish this by exporting two path prefixes:

const (
  HaberdasherPathPrefix = "/notreal.example/Haberdasher/"
  HaberdasherLegacyPathPrefix = "/twirp/notreal.example.Haberdasher/"
)

Users can mount both on a mux if they want to support old clients. If not, they can just use the new one.

If we do this, there are other consequences.

  • Documentation would need to be updated everywhere.
  • Server implementations in other languages would need to be updated ASAP, as new clients won't work with old servers.
  • If people have log parsing or metrics gathering code that operates on routes, they'd need to change them.

The pain of those other consequences is a good reason that we should try to get this out promptly, if we do it.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions