Flexible URI structure in AP Server Implementations

This is something I’ve been thinking about for a while and I’d be interested in hearing thoughts from other AP developers. I run a single user Mastodon instance on a personal domain. I’m not happy with the Mastodon resource usage and I’d like to move to a lighter implementation. My goal is to move my domain with all my content to this new AP server implementation. I want this to be completely transparent to other AP network nodes (requires no changes to their implementations).

Theoretically, this should be as easy as exporting my Mastodon data (maybe directly from the database) and loading it into the new server’s data store. However, the challenge I’ve seen is that AP servers typically hard-code their URI path structures. For example, a note on one server might require a URI structure like

https://server.example/users/steve/statuses/<id>

and on another server, it might require a structure like:

https://server.example/steve/p/<id>

This means I can’t simply import content using the first URI structure into a server requiring a different one.

I typically see server implementations that define route handlers that depend on the URI path structure. Something like…

@app.get("/actors/:id")
def get_actor(request): ...

@app.get("/actors/:id/followers")
def get_followers(request): ...

Based on my experimentation, this seems unnecessarily rigid although I understand it’s the common way of thinking about writing API route handlers.

What if… there were one AP POST route handler and one AP GET route handler? The request URL is the AP URI. This URI can be used to internally dereference the target resource and do routing and validation based on that resource independently of the structure of the HTTPS URI. Something like (pseudocode):

@app.get("/:path")
def get(request): ...
   uri = get_uri(request)  # adds the scheme://domain prefix
   resource = dereference(uri)
   return serialize_to_jsonld(resource)

@app.post("/:path")
def post(request): ...
   uri = get_uri(request)  # adds the scheme://domain prefix
   resource = dereference(uri)
   # validate target resource
   # perform side-effects

For a GET, the server would attempt to dereference the resource based on the URI and serialize it to AP JSON-LD.

For a POST, the target resource is an inbox or outbox. Given the URI, the server can dereference the target resource and verify that it’s a OrderedCollection. Assuming an attributedTo property is defined for the box (a good idea in any case, IMO), the actor can be dereferenced and the server can determine if the box is an inbox, outbox or an invalid resource.

At that point the processing can continue without any dependencies on a specific URI structure.

When local resources are created, a server may have a preferred URI structure but that wouldn’t prevent others from being supported in the same implementation. A server could even allow each installation or even each user to select from a set of preferred URIs in a similar way to WordPress supports different permalink structures.

The other potential issue is that some servers implement resource stores that depend on a specific URI structure (like subdirectories or tables that correspond to a URI path structure). To be flexible, the resource store should be able to dereference a resource based on an opaque URI. For example, a server that uses files for storage might hash the URI and use that as the resource file name.

The shift in perspective here is thinking in terms of resource URIs instead of URL endpoint routes.

Thoughts?

3 Likes

This is more or less how my own toy AP server works. The Fediverse is effectively treated as a key-value store where the keys are URIs and the values are JSON objects. My server has a set of URI prefixes which it considers itself to be “authoritative” for, but beyond that, it doesn’t impose any structure on the URIs.

I suppose an important question is - in what form do you want to store your objects? In ActivityPub format? Or in your own data model, which you convert to ActivityPub when another site requests them? My project is meant to be a generic ActivityPub server; converting to and from an application-specific data model is the job of that application. If you’re going with an application-specific design then the classical endpoint-based approach might be a better fit.

3 Likes

As you said (or strongly implied), AS2 is a serialization format. It doesn’t constrain how the data is internally represented in an application. However it’s internally represented, the application must be able to serialize to/from AS2 (maybe with extensions) or it’s not an ActivityPub server.

I also have a toy server prototype that implements this approach. The storage is pluggable and I’ve implemented resource stores using everything from flat-files and MongoDB to RDF graph stores. However, I think what I’m proposing is mostly independent of the resource store implementation unless that store’s resource retrieval depends on specific URI path structures. Even in a very conventional relational database store, it’s usually feasible to add a URI column for dereferencing. Even Mastodon has that. :wink:

The flexible approach only requires that an application can externally and internally dereference a resource from a URI (needed externally for AP GET anyway) and that the AP POST handler can introspect a dereferenced resource to determine if it’s a valid POST target (inbox/outbox). To some extent, the classical path-based endpoints are doing part of the inbox/outbox validation in the request router (given an app-specific URI path pattern). In the flexible approach, this moves into the POST handler.

Does that mean it has multi-tenant/domain support? Although it’s somewhat orthogonal, the resource-oriented design also makes it easier to support multiple domains. In my prototype, a URI maps to an “instance” based on a URI prefix (scheme://domain[:port]/). That prefix is the URI for the instance resource and that resource can be configured independently from other instances (name, presentation themes, server rules, storage destinations, webfinger, nodeinfo, etc.). One nice feature of this approach is that the remote resource cache can be shared between the instances, which lowers storage space requirements.

Yes - the interface to the storage is quite simple and easy to abstract out. The processing of GETs and POSTs doesn’t need to know very much about how the storage is actually implemented.

Yup. It also supports non-HTTPS schemes, in anticipation of implementing FEP-ef61 one day.

1 Like

This is how FEP-ef61 gateway is implemented. Server has no authority over a portable object, and it sees the path component of a portable identifier as an opaque string. In this situation route-based request handling can’t be used and there have to be catch-all GET and POST handlers.

That works fine for actors and objects, but I haven’t yet found a satisfactory solution for collections.

This approach should work with regular HTTP URIs too, and I think using it is a good idea because it enables cross-software portability.

How do you deal with collections? For example, when actor publishes an activity, do you overwrite the entire outbox collection in your kv store?

Is this not already the case? On the Web, resources have URIs, and “endpoint routes” are for applications. The mix-up happens when applications are used to generate virtual resources.

What you can do is use redirects, I guess. You will probably need a basic HTTP server or some such application that handles requests for “old” URIs and redirects to the “new” URIs.

The part I’m not 100% sure about is how widespread is the support across fedi for not choking on 3xx responses. We can say that every user agent should support redirects, but the reality is that it’s possible that some fedi user agents out there are so naive that they can’t handle a simple redirect. Or they do some unnecessary “validation” by checking to see if the requested URI matches the object id. It wouldn’t surprise me to find out at least one implementation out there does this. (They’re wrong for doing it, but a lot of applications are wrong at a lot of things.)

Still, if you’re talking about the general HTTP case, then pretty much every single HTTP agent can handle redirects. You’d have to do something special to not support redirects. At the very least, it would work in a standard Web browser like Firefox or Chromium. :stuck_out_tongue:

The issue I’m discussing is related to the application endpoint routes being tightly coupled to a specific URI structure.

My issue is related mostly to server-to-server federation rather than browser-to-server communication. Even though it’s not uncommon for server-side HTTP libraries to make redirect behavior opt-in, I agree that part is easy. It’s the validation that will likely cause problems. Seeing a different id than the requested URI is one issue (as you mentioned), but some servers may also use the request URI to determine “ownership” and related authorization (rather than the different id in the dereferenced resource).

Do you know of anyone that’s done a lossless move of their domain’s resources from one AP implementation to another using redirects? I’d be interested in seeing some kind of experience report.

The other downside of redirects is that it would require me transforming my entire migrated data set to use the target server’s hard-coded URI structure. This is possible, but nontrivial. With the flexible approach I’m describing, I’d be able upload my dataset into the new server as-is. With redirects, my resources now effectively have multiple URIs floating around the Fedi. In at least that sense, it’s also not as transparent as what I’m suggesting.

I see redirects as a workaround for hard-coded URI structures. I’m hoping this thread will give developers implementing new servers something to think about. However, in the short term, I either need to find a Mastodon API-compatible server implementation that supports this or that can be modified relatively easily to use a different URI structure. I currently don’t have much hope that the former exists in the Fedi.

(I’ve investigated one server implementation already and unfortunately it has dependencies on a hard-coded URI path structure throughout the code base, not just in route handlers.)

1 Like

Basically they’re a separate primitive, more set-like - the storage interface has (abstract) methods for “add to / remove from collection”, “is URI in collection?” and “list all members of collection”.

2 Likes

When you handle an incoming request, do you look up target URI twice, once in object index, and once in collection index?

Same thing imo. Servers are using HTTP libraries to act as HTTP user agents, which makes them web browsers.

Again, I really hope this isn’t as big of an issue as I am assuming it could be, but the only way to know for sure is to audit every single implementation you care to interoperate with. I am merely claiming that attempting to validate ids is a footgun. If you wouldn’t do it for an HTML document, why do it for an AS2 document?

To further the analogy, the use of id is like having a rel=canonical link (or possibly a singular rel=self link) in some arbitrary HTML. If AP passed around HTML documents you could imagine that “MUST have global identifier” would be interpreted by inserting a rel=canonical or rel=self link tag. “Ownership” might be established by rel=author or similar, and a “same-origin” check would validate that the author.href and canonical.href (or any self.href) are on the same web origin. But just saying this out loud seems kind of ridiculous, doesn’t it? The real “same-origin” check shouldn’t take rel=author into account at all, much less check their domain. The only thing you can say for sure is that if any of the self-links (including canonical) has a certain authority component, and you fetched from an HTTP Host that is equivalent to that, then the response is authoritative for that URI, no matter what the resource contains.

For example:

<html>
<head>
<link rel="canonical" href="https://abdullahtarawneh.com/">
<link rel="self" href="https://abdullahtarawneh.com/">
<link rel="self" href="https://tarawneh.org/">
</head>
</html>
{
  "@id": "https://abdullahtarawneh.com/",
  "https://www.w3.org/ns/activitystreams#alsoKnownAs": [
    {"@id": "https://tarawneh.org/"}
  ]
}

If I did GET / HTTP/1.1 against Host: abdullahtarawneh.com and received either of the above documents, it would be enough to establish bidirectional verification of the link between document and identifier. In this case, the claim being verified is that https://abdullahtarawneh.com/ is the identifier of the document, and the way it is being verified is with an HTTP GET that should result in that document.

If I did GET / HTTP/1.1 against Host: tarawneh.org and received either of the above documents, it would still be equivalent to the above. Notably, this holds true even though https://tarawneh.org/ redirected to https://abdullahtarawneh.com/. It could redirect to something else, but in that case the second HTTP request would no longer be authoritative. Basically, there are two authoritative requests going on here:

  • https://tarawneh.org/ returns 302 Found with a Location: https://abdullahtarawneh.com/ header. This HTTP response is authoritative on the host tarawneh.org.
  • https://abdullahtarawneh.com/ returns 200 OK with an above document. This HTTP response is authoritative on the host abdullahtarawneh.com.

From the perspective of the agent fetching the resource, it is irrelevant whether there is a redirect or not. The thing you care about is the end resource’s content, which should contain a link back to the identifier you used to fetch it. So the HTML and JSON-LD responses are both “authoritative” in the sense that you can start with either identifier and end up with a document that claims to represent that resource.

If the redirect instead passed through https://trwnh.com/, and returned the same document(s), then the document could be considered representative of https://tarawneh.org/ but NOT https://trwnh.com/, because https://trwnh.com/ is not one of the self-links.


In any case, a lot of this stuff doesn’t matter (or really, REALLY shouldn’t matter) if you’re keeping the domain. “Ownership” or “same-origin” or “authoritative” or any of those other things shouldn’t come into play at all if you’re, say, redirecting from https://server.example/users/steve/statuses/123 to https://server.example/steve/p/456. The only challenging thing would be maintaining a mapping of post ids, since some servers might use Snowflake and some other servers might use UUIDs and some other servers might use flakeID and so on.

Well, someone has to be the first! Maybe start small, set up a few test instances, do some controlled tests. See how various softwares respond to redirects. Probably pretty daunting for a “small” scale test, but this is fedi we’re talking about, so…

This is mostly a consequence of the data being generated on-the-fly, right? Unless you wanted to patch your new server-of-choice to take some kind of ap_id database field and use that to populate the id of objects. I think you can keep all the old representations and id-references exactly the same without rewriting them into the new format, but the server has to be aware of this.

If anything, perhaps a facade server could also help here? Just an HTTP server that proxies or forwards HTTP requests as-is to your backend of choice. Or otherwise fetches the contents from the current backend, then manipulates the resource to inject the appropriate values in the right places.

For example, say you’re moving Mastodon to Pleroma, and you run this “facade server” configured to handle old routes. Say with something like nginx:

upstream facade {
  server facade.local:443;
}

location ^~ /users/steve/statuses/ {
  proxy_pass http://facade;
}
GET /users/steve/statuses/113547021337474328 HTTP/1.1
Host: social.technoetic.com

The “facade server” looks in its lookup table, sees that /users/steve/statuses/113547021337474328 is currently being served by /objects/396c6790-022a-4471-9d1a-a6ae9b9c3de9, and fetches that from Pleroma, essentially treating Pleroma as a “backend API” of sorts.

GET /objects/396c6790-022a-4471-9d1a-a6ae9b9c3de9 HTTP/1.1
Host: social.technoetic.com

{
  "@context" [...],
  "id": "https://social.technoetic.com/objects/396c6790-022a-4471-9d1a-a6ae9b9c3de9",
  ...
}

It then responds with a modified document that swaps out the id:

HTTP/1.1 200 OK

{
  "@context" [...],
  "id": "https://social.technoetic.com//users/steve/statuses/113547021337474328",
  ...
}

But really though… it is my sincerest hope that none of this is remotely necessary.

yes, shitposter.club was migrated from GNU Social to Pleroma. There were problems with fetching some older things, but those were later fixed and had more to do with OStatus to Acticvity Pub than switching implementations iirc. There are also examples of Mastodon to Pleroma migrations.

In both cases extra code in Pleroma was needed. Support for pasword hashes used by the other implementation was added (but that’s not AP specifiec and getting people to set a new password may be a viable option for you), as well as redirects/extra routes.

You also have to wonder about Activity Pub id’s who are being send out. E.g. if your “old” implementation builds the actor id on federating as "https://" + domain + "/users/" + username, but you “new” implementation does something like "https://" + domain + "/objects/" + object_uuid, then you can write a redirect from the old format to the new, but you also have to make sure that the “old” Activity Pub id is still used when federating out, otherwise it will be considered a new actor. I believe this wasn’t much of a problem with GNU Social/Pleroma/Mastodon because they all based there uri’s on GNU Social (or maybe it was because Pleroma stores the actual ap id’s, or maybe a combination of both, idk).

At any rate, in both cases the job was non-trivial and took a lot of effort to pull of. You basicallly have to “translate” the database structure of the first implementation into the second, and then make sure the Acticvity Pub parts still work. But, yes, it has been succesfully done. Here’s an old issue i could still find about it Mastodon -> Pleroma server migration script (#162) · Issues · Pleroma / pleroma · GitLab

Yeah, that sounds like a potential problem with a basic redirect.

Thanks for sharing that. I’m considering whether it’s less work to modify my own server code to implement the parts of the Mastodon client API I need to support the clients I use. This is a single user migration (me) so password hashes are not an issue. I just want to move my own actor and generated content (personal domain).

1 Like

Isn’t this basically the same as what was discussed here? I personally don’t think FEP-ef61 is a solution and the idea of just redirecting to all the old content using the old URIs is not really feasible - even if you do that, you could still run into a situation where your old implementation and your new implementation have overlapping URIs and then you’re out of luck.

What I’ve basically concluded from this is that the freedom to choose any URI an implementation wants as IDs for objects was a mistake. It would’ve been much better if all implementations were required to use a .well-known path. Then all paths across all implementations would be the same. So if I have a user on domain.example then its ID could (for example) be https://domain.example/.well-known/ap/users/58b5cbaf-069b-4ee8-bdb2-3c0d381ac8ce or something like that - and if I changed my implementation, it would be the exact same path after the change.

This design choice of ActivityPub can’t really be changed in any backwards-compatible manner, so unfortunately I don’t see any realistic path forward where this will ever be feasible. I gave up on my own ActivityPub implementation effort once I realized this as I see it as too much of a dealbreaker.

Hopefully some AP 2.0 or spiritual successor will one day fix this, but that seems very, very far away indeed.

1 Like

@SorteKanin

What problems do you expect with the proposed ActivityPub application design (treating paths as opaque strings)?

Yes, a path that is valid in one application might be "reserved" in another application. For example: /api. But we can simply agree to not use certain paths, or agree to use a certain prefix, perhaps even .well-known. That doesn't require changes in ActivityPub spec, and can be written in a FEP.

Is there anything else?

@protocol

I’m not sure what you’re asking, can you be more specific? What proposed AP application design?

I mean it kinda does require changes in the AP spec if you ask me, because anyone is free to ignore any FEP and just use whatever paths they want. This needs to be in the base level of the protocol and not as an extension, as you otherwise have no guarantees. Relying on an FEP to fix this is not feasible I think because:

  1. It makes it an opt-in thing rather than a mandatory thing, which will split the whole ecosystem into two or more camps.
  2. It leaves all the existing behind with no migration path (though not sure how that would be overcome even with mandatory paths).
1 Like

Not using hard-coded AP URI path structures for HTTP request routing (see the first post in the thread for more details).

Developers are free to ignore the AP/AS2 specs too (and some major implementations have ignored mandatory parts of it), but that’s a whole different discussion. :wink:

However, reserved paths (or reserved URL prefixes) are a different concept than mandatory paths (which nobody is suggesting AFAIK).

3 Likes