public
Created

  • Download Gist
gistfile1.md
Markdown

App Framework URL Jazz

Gathered thoughts about URL woes at the various levels in our stack of components which deal with them. URLs are hard to get right, but it is important to do so.

Application-level URL Handling

Public methods that change the URL:

  • save() -> _save()
  • replace() -> _save()
  • upgrade() -> replace()
  • navigate() -> save()

So, eventaully everything goes to _save().

Default _save() Behavior (Router)

Router's default implementation of _save() will either use HTML5 pushState() or set window.location.hash. A security limitation exists when using HTML5 pushState() which does not exist for hash-based URLs, this being the same-origin policy. This method gets around of ever causing the browser to throw the security error by always assuming the URL passed-in is from the same-origin and ignores everything before the path-part.

// Current page: http://example.com/

// Will happlily change the URL to: http://example.com/foo/
router.save('http://www.google.com/foo/');

App's "enhanced" _save() Behavior

App overrides _save() to do some extra things in the spirit of progressive-enhancement. The contract with _save() is to add or replace a history entry; so can also be accomplished by assigning a new URL to window.location or calling window.location.resplace(). To make your app a good citizen of the web, App adds a serverRouting attribute that cues App on whether or not the server can handle requests to all full-path URLs for the app. When serverRouting is true, and html5 is false, these fallbacks are used instead of using hash-based URLs.

Should navigate() and save() have the same result?

I can see the argument for the example above where save() with an absolute URL of a different origin simply ignores the origin difference and just uses the path-part of the URL. But I don't think a method called navigate() should do this!

// Current page: http://example.com/

// Seems wrong that this would go to: http://different.domain.com/foo/
app.navigate('http://different.domain.com/foo/');

It could be argued that this is silly, developers would never do this, but think about this case:

// Current page: http://example.com/
// Navigate to a secure page:
app.navigate('https://example.com/sign-in/');

// Currently this would just go to: http://example.com/sign-in/ (not secure!)

These two URLs have different origins because their schemes are different.

Route's hasRoute() Method with Absolute URLs

We recently made sure that hasRoute() will not barf when passed an absolute URL, but its behavior is questionable like navigate()'s.

var router = new Y.Router({
    routes: [
        {path: '/foo/', callback: function () {}}
    ]
});

// Current URL: http://example.com/
// Should `router` have a route for: http://www.google.com/foo/
route.hasRoute('http://www.google.com/foo/'); // => true!?

The router is reporting that it does in fact have a route handler for a URL which is at a completely different origin, this seems really confusing.

Things I'm Thinking About

Behavior of navigate()

  • What should navigate() do when called with a URL of a different origin from the current page?

    If it should go to that page using window.location, does this mean the default navigate behavior would either save() to the router, or use window.location. Should Pjax's link-click handler then always fire the navigate event, even when no route matches? <- That would seem weird when multiple Y.Apps are on the page.

    It seems like the navigate event should only fire if there is a matching route to handle it. Inside of the navigate() method it can simply assign the absolute URL of a different origin to window.location, or check that a route matches and fire the navigate event.

  • What happens when an App is using serverRouting: false, and a link like: <a href="#/foo/bar/"></a> is clicked?

    Should the App dispatch to the route handlers for this (assuming there is one)?

    What if the link starts with "/": <a href="/#/foo/bar/"></a>?

  • Calling navigate() with a relative URL (path-only, does not start with /) when serverRouting is undefined or false will not resolve the URLs correctly. Host-relative URLs (at least) must be used!

Router's Handling of Absolute URLs

  • If Router does not care about same-origin, that should be documented.

  • What should Router do if it is asked to save() a URL of different origin (the method is chainable)?

    Since this is asked to save a new history entry, what is the correct thing to do? Nothing? Throw an error like pushState() would if called with a URL of different origin?

  • Should router always resolve URLs (making them absolute)?

General URL Stuff

  • When a URL is considered resolved, it should be absolute.

    • Scheme-relative URLs (starts with //) can use the current window.location.protocol.

    • Host-relative paths (starts with /) can be considered normalized.

  • The page's origin should can be considered constant for the YUI instance.

  • The path-root for normalizing paths should be considered dynamic.

  • Relative URLs (path that doesn't start with /), needs to be resolved against the current path-root.

Thanks for digging into this. My thoughts:

  • Router should probably log an error (but not throw) when save() is given a URL with a different origin.

  • I feel like navigate() should also log an error, except when the only difference is the scheme (http <-> https). Conceptually, it doesn't make any sense to tell an app to navigate to an origin out of that app's control, but if the only difference is the scheme, then we should make that work.

  • Pjax's link click handler should not fire the navigate event when no route matches.

  • When serverRouting is false and a hash URL is clicked, I think the app should attempt to handle it, and upgrade to an HTML5 URL if possible. If the URL is not a pure hash URL (i.e. /#/foo/bar) then it should be handled just like any other real URL (as if it were an HTML5 URL and not a hash URL).

  • I'm not sure whether Router should always resolve URLs. I'm inclined to say no, and that it should only do it where it's necessary, but then again if always resolving URLs is relatively easy and doesn't hurt anything (thus reducing complexity), then maybe it should.

Router should probably log an error (but not throw) when save() is given a URL with a different origin.

Makes sense, sounds like a good middle-ground; I'll also make sure it does not update the URL in this case too.

Which method in Router do you think makes the most sense to handle this? My gut says the protected _save() method, unless there is a plan to wrap events around save() and replace().

I feel like navigate() should also log an error, except when the only difference is the scheme (http <-> https). Conceptually, it doesn't make any sense to tell an app to navigate to an origin out of that app's control, but if the only difference is the scheme, then we should make that work.

And when the schemes are different, it will then assign the new URL to window.location causing a full-page reload. Should it only do this if there's a matching route for the URL's path?

Pjax's link click handler should not fire the navigate event when no route matches.

There is a prerequisite here to define how Router's hasRoute() method should handle absolute URLs, i.e. should it apply the origin contraint?

When serverRouting is false and a hash URL is clicked, I think the app should attempt to handle it, and upgrade to an HTML5 URL if possible. If the URL is not a pure hash URL (i.e. /#/foo/bar) then it should be handled just like any other real URL (as if it were an HTML5 URL and not a hash URL).

When serverRouting is explicitly false, upgrading should not happen. When it is undefined, I agree, it should try to upgrade to an HTML5 URL if possible.

Which method in Router do you think makes the most sense to handle this? My gut says the protected _save() method, unless there is a plan to wrap events around save() and replace().

_save is the right place. I do plan to add events, but those can be dispatched from _save as well.

And when the schemes are different, it will then assign the new URL to window.location causing a full-page reload. Should it only do this if there's a matching route for the URL's path?

Yep.

There is a prerequisite here to define how Router's hasRoute() method should handle absolute URLs, i.e. should it apply the origin contraint?

Yes, hasRoute() should apply the origin constraint (but should allow a differing scheme), and should only return true if there's a route that matches the URL.

When serverRouting is explicitly false, upgrading should not happen. When it is undefined, I agree, it should try to upgrade to an HTML5 URL if possible.

Ah, right. I was confusing the false and undefined cases.

Yes, hasRoute() should apply the origin constraint (but should allow a differing scheme), and should only return true if there's a route that matches the URL.

Okay, I can see how hasRoute() being more liberal makes sense w.r.t. route-paths in an application; but I do worry people may use this method as a pre-flight check before passing that same URL to save() which would lead the the logged error if the schemes are different.

Details, but you know, that's where the devil is :)

Argh, I was confusing save() and navigate(). You're right, Router's hasRoute() should do a strict same-origin check and return false even if only the scheme is different. Maybe App should override hasRoute() to allow differing schemes, but it's probably not worth it if it will confuse people.

Looking at how Router's save() and replace() APIs are document, I see two reasonable paths forward:

@param {String} [url] URL to set. Should be a relative URL. If this
  router's `root` property is set, this URL must be relative to the
  root URL. If no URL is specified, the page's current URL will be used.
  1. Go with how we have been leaning in this discussion, to make Router's APIs that handle URLs more robust and give them the ability to handle absolute URLs and preform same-origins checks.

  2. Leave Router to stay focused on strictly path-based routing and be less concerned about its correctness with handling absolute URLs and adhering to same-origin contraints. Instead keep all of the URL handling logic higher in the stack, within PjaxBase, and make sure anything that calls into Router's APIs does the necessary pre-flight checks.

I still think we are on the right path going with #1 because robust URL-handling should be lower-level so things built on top of it can stay concerned about their value-add (like Pjax), and not be concerned with the intricacies of dealing with URLs. I think this will lead to less confusion about what people to pump through Router's APIs since the more resilient API would not be as dependent on the particulars of the instance's configuration.

I just want to make sure your okay with a more robust Router URL API, and that you didn't want to retain its naive URL purity :)

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.