Skip to content
This repository has been archived by the owner on May 17, 2019. It is now read-only.

isSSR should not render as template if the request implies a non-html MIME type #116

Closed
lhorie opened this issue Jan 31, 2018 · 6 comments
Closed
Assignees
Labels

Comments

@lhorie
Copy link
Contributor

lhorie commented Jan 31, 2018

Currently, there's no good way of rendering a dynamic PDF other than doing horrible hacks.

We already do the right thing for the .js extension:

if (ctx.path.match(/\.js$/)) return false;

This logic should be extended to other MIME types, such as PDF, images, fonts, etc

@lhorie
Copy link
Contributor Author

lhorie commented Jan 31, 2018

Related: #45, #46

@mlmorg
Copy link

mlmorg commented Feb 23, 2018

Need to come up with potential solutions.

@lhorie
Copy link
Contributor Author

lhorie commented Mar 1, 2018

Permutations that work today

  • SSR'ed url with extension with accepts header - /home.html - probably doesn't matter
  • SSR'ed url without extension with accepts header - /home
  • SSR'ed url with extension without accepts header - /home.html - breaks w/ curl
  • SSR'ed url without extension without accepts header - /home - breaks w/ curl
  • Dynamic asset with extension with accepts header - /some-dynamic-html-asset.html - e.g. dynamic PDF for download
  • Dynamic asset without extension with accepts header - /some-dynamic-asset - probably doesn't matter
  • Dynamic asset with extension without accepts header - /some-dynamic-html-asset.html - e.g. dynamic PDF for download
  • Dynamic asset without extension without accepts header - /some-dynamic-asset - probably doesn't matter
  • Static asset with extension with accepts header - /some-dynamic-html-asset.html
  • Static asset without extension with accepts header - /some-dynamic-asset - probably doesn't matter
  • Static asset with extension without accepts header - /some-dynamic-html-asset.html
  • Static asset without extension without accepts header - /some-dynamic-asset - probably doesn't matter
  • Endpoint with extension with accepts header - /api/foo.json - currently breaks if open in new tab (e.g. for debugging)
  • Endpoint without extension with accepts header - /api/foo - currently breaks if open in new tab (e.g. for debugging)
  • Endpoint with extension without accepts header - /api/foo.json
  • Endpoint without extension without accepts header - /api/foo

pros: it's what we have today
cons: doesn't work for asset downloads, and does not address debugging devexp issues

Covered cases: 8/16

Proposed solutions so far and what scenarios they fix

extension regex check

ctx.path.match(/\.(js|gif|jpg|png|pdf|html|json)$/)

  • SSR'ed url with extension with accepts header - /home.html
  • SSR'ed url without extension with accepts header - /home
  • SSR'ed url with extension without accepts header - /home.html
  • SSR'ed url without extension without accepts header - /home
  • Dynamic asset with extension with accepts header - /some-dynamic-html-asset.html
  • Dynamic asset without extension with accepts header - /some-dynamic-asset
  • Dynamic asset with extension without accepts header - /some-dynamic-html-asset.html
  • Dynamic asset without extension without accepts header - /some-dynamic-asset
  • Static asset with extension with accepts header - /some-dynamic-html-asset.html
  • Static asset without extension with accepts header - /some-dynamic-asset
  • Static asset with extension without accepts header - /some-dynamic-html-asset.html
  • Static asset without extension without accepts header - /some-dynamic-asset
  • Endpoint with extension with accepts header - /api/foo.json
  • Endpoint without extension with accepts header - /api/foo
  • Endpoint with extension without accepts header - /api/foo.json
  • Endpoint without extension without accepts header - /api/foo

pros: covers all classes of use cases (ssr, dynamic asset, static asset, endpoint)
cons: does not address debugging devexp issues

Covered cases: 12/16

extension convention (no-extension = ssr, extension = non-ssr) in addition to accepts header heuristics

pros: same as extension regex check
cons: same as extension regex check

Covered cases: 12/16

asset manifest check

  • SSR'ed url with extension with accepts header - /home.html
  • SSR'ed url without extension with accepts header - /home
  • SSR'ed url with extension without accepts header - /home.html
  • SSR'ed url without extension without accepts header - /home
  • Dynamic asset with extension with accepts header - /some-dynamic-html-asset.html
  • Dynamic asset without extension with accepts header - /some-dynamic-asset
  • Dynamic asset with extension without accepts header - /some-dynamic-html-asset.html
  • Dynamic asset without extension without accepts header - /some-dynamic-asset
  • Static asset with extension with accepts header - /some-dynamic-html-asset.html
  • Static asset without extension with accepts header - /some-dynamic-asset
  • Static asset with extension without accepts header - /some-dynamic-html-asset.html
  • Static asset without extension without accepts header - /some-dynamic-asset
  • Endpoint with extension with accepts header - /api/foo.json
  • Endpoint without extension with accepts header - /api/foo
  • Endpoint with extension without accepts header - /api/foo.json
  • Endpoint without extension without accepts header - /api/foo

pros: addresses any type of static assets
cons: does not address asset downloads and does not address debugging devexp issues

Covered cases: 10/16

unset ctx.element and ctx.template if ctx.body is set

pros: addresses all cases
cons: brittle (no guarantee it won't throw errors on arbitrary plugin order), hacky (least bad hook requires monkeypatching the koa getter of ctx.body), inneficient (it's still possible to run SSR code before ctx.body is set due to plugin ordering)

Covered cases: 16/16 but poorly


Other concerns

It may also be desirable to show user-friendly 404s for a non-existing route, regardless of whether it would have been ssr'ed via other heuristics.

This requires the ability to arbitrarily override the isSSR check, and could conceivably be supported via DI

app.register(IsSSRToken, ctx => !ctx.path.match(/paths|with|no|ssr/))

pros: addresses all cases
cons: requires manual intervention, potential duplication of routing logic

Covered cases: 16/16


Analysis

  • Developer patterns (curl, opening an endpoint from the browser) are difficult to support
  • Asset manifest is better than current strategy but does not handle dynamic endpoints
  • Extension-based heuristics provide the largest amount of coverage without hacks/caveats. Unsupported cases often align with use cases that have no value.
  • Heuristics based on the koa context state machine are likely to break in difficult to understand ways

Recommendation

  1. Use an extension-based heuristic (either a blacklist of extensions where ssr should never occur, or generically avoid ssr in any url w/ an extension) to unblock users that need dynamic endpoints and provide a nicer experience out of the box.
  2. Provide a isSSR DI hook for power users
  3. Iterate on potential solutions to the difficult-to-support cases later if the two steps above are not sufficient.

@rtsao
Copy link
Member

rtsao commented Mar 1, 2018

There's something I find really appealing about a solution using a path-prefix, (e.g. a route is SSR if and only if it does not start with _). IMHO, if you are not serving something to be viewed in a browser, then you should not care that much about the URL. Path prefix is dead-simple heuristic and I just don't see the constraint being an issue in any practical case.

I'm less enthused about an extension-based convention, since there's more ambiguity in the case of .html/.htm extensions, which people often expect to be equivalent to the extensionless path for web pages. Add in the common magic regarding rewrite rules in frontends and it comes even more confusing.

@slonoed
Copy link

slonoed commented Mar 1, 2018

@rtsao I think prefix doesn't conform web ideology. URL doesn't imply type, headers do. It also means that routes with _ prefix are forbidden and application which want to use generated routes have to escape this symbol.

Example: I have an application which is more or less file browser (think about s3 file browser). And I want to show file path as URL path, like https://example.com/any/name/path. A path https://example.com/_any/name/path is broken now. To avoid this I have to replace symbol (which is super weird) or create prefix path https://example.com/files/_any/name/path.

Take a look at this comment #45 (comment), maybe it could be a better way.

@KevinGrandon
Copy link
Contributor

We have implemented both parts 1) and 2) of your suggestion. I think we'll still need to iterate on this to make it nicer for entries in the asset manifest, and also potentially per-plugin setting of headers. People should generally be unblocked now, and we can continue making this better over time.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

5 participants