###*
Server protocol
===============
You rarely need to change server-side code
in order to use Unpoly. There is no need to provide a JSON API, or add
extra routes for AJAX requests. The server simply renders a series
of full HTML pages, just like it would without Unpoly.
That said, there is an **optional** protocol your server can use to
exchange additional information when Unpoly is [updating fragments](/up.link).
While the protocol can help you optimize performance and handle some
edge cases, implementing it is **entirely optional**. For instance,
`unpoly.com` itself is a static site that uses Unpoly on the frontend
and doesn't even have a server component.
## Existing implementations
You should be able to implement the protocol in a very short time.
There are existing implementations for various web frameworks:
- [Ruby on Rails](/install/rails)
- [Roda](https://github.com/adam12/roda-unpoly)
- [Rack](https://github.com/adam12/rack-unpoly) (Sinatra, Padrino, Hanami, Cuba, ...)
- [Phoenix](https://elixirforum.com/t/unpoly-a-framework-like-turbolinks/3614/15) (Elixir)
## Protocol details
\#\#\# Redirect detection for IE11
On Internet Explorer 11, Unpoly cannot detect the final URL after a redirect.
You can fix this edge case by delivering an additional HTTP header
with the *last* response in a series of redirects:
```http
X-Up-Location: /current-url
```
The **simplest implementation** is to set these headers for every request.
\#\#\# Optimizing responses
When [updating a fragment](/up.link), Unpoly will send an
additional HTTP header containing the CSS selector that is being replaced:
```http
X-Up-Target: .user-list
```
Server-side code is free to **optimize its response** by only returning HTML
that matches the selector. For example, you might prefer to not render an
expensive sidebar if the sidebar is not targeted.
Unpoly will often update a different selector in case the request fails.
This selector is also included as a HTTP header:
```
X-Up-Fail-Target: body
```
\#\#\# Pushing a document title to the client
When [updating a fragment](/up.link), Unpoly will by default
extract the `
` from the server response and update the document title accordingly.
The server can also force Unpoly to set a document title by passing a HTTP header:
```http
X-Up-Title: My server-pushed title
```
This is useful when you [optimize your response](#optimizing-responses) and not render
the application layout unless it is targeted. Since your optimized response
no longer includes a ``, you can instead use the HTTP header to pass the document title.
\#\#\# Signaling failed form submissions
When [submitting a form via AJAX](/form-up-target)
Unpoly needs to know whether the form submission has failed (to update the form with
validation errors) or succeeded (to update the `up-target` selector).
For Unpoly to be able to detect a failed form submission, the response must be
return a non-200 HTTP status code. We recommend to use either
400 (bad request) or 422 (unprocessable entity).
To do so in [Ruby on Rails](http://rubyonrails.org/), pass a [`:status` option to `render`](http://guides.rubyonrails.org/layouts_and_rendering.html#the-status-option):
class UsersController < ApplicationController
def create
user_params = params[:user].permit(:email, :password)
@user = User.new(user_params)
if @user.save?
sign_in @user
else
render 'form', status: :bad_request
end
end
end
\#\#\# Detecting live form validations
When [validating a form](/input-up-validate), Unpoly will
send an additional HTTP header containing a CSS selector for the form that is
being updated:
```http
X-Up-Validate: .user-form
```
When detecting a validation request, the server is expected to **validate (but not save)**
the form submission and render a new copy of the form with validation errors.
Below you will an example for a writing route that is aware of Unpoly's live form
validations. The code is for [Ruby on Rails](http://rubyonrails.org/),
but you can adapt it for other languages:
class UsersController < ApplicationController
def create
user_params = params[:user].permit(:email, :password)
@user = User.new(user_params)
if request.headers['X-Up-Validate']
@user.valid? # run validations, but don't save to the database
render 'form' # render form with error messages
elsif @user.save?
sign_in @user
else
render 'form', status: :bad_request
end
end
end
\#\#\# Signaling the initial request method
If the initial page was loaded with a non-`GET` HTTP method, Unpoly prefers to make a full
page load when you try to update a fragment. Once the next page was loaded with a `GET` method,
Unpoly will restore its standard behavior.
This fixes two edge cases you might or might not care about:
1. Unpoly replaces the initial page state so it can later restore it when the user
goes back to that initial URL. However, if the initial request was a POST,
Unpoly will wrongly assume that it can restore the state by reloading with GET.
2. Some browsers have a bug where the initial request method is used for all
subsequently pushed states. That means if the user reloads the page on a later
GET state, the browser will wrongly attempt a POST request.
This issue affects Safari 9 and 10 (last tested in 2017-08).
Modern Firefoxes, Chromes and IE10+ don't have this behavior.
In order to allow Unpoly to detect the HTTP method of the initial page load,
the server must set a cookie:
```http
Set-Cookie: _up_method=POST
```
When Unpoly boots, it will look for this cookie and configure its behavior accordingly.
The cookie is then deleted in order to not affect following requests.
The **simplest implementation** is to set this cookie for every request that is neither
`GET` nor contains an [`X-Up-Target` header](/#optimizing-responses). For all other requests
an existing cookie should be deleted.
@class up.protocol
###
up.protocol = (($) ->
u = up.util
###*
@function up.protocol.locationFromXhr
@internal
###
locationFromXhr = (xhr) ->
xhr.getResponseHeader(config.locationHeader) || xhr.responseURL
###*
@function up.protocol.titleFromXhr
@internal
###
titleFromXhr = (xhr) ->
xhr.getResponseHeader(config.titleHeader)
###*
@function up.protocol.methodFromXhr
@internal
###
methodFromXhr = (xhr) ->
if method = xhr.getResponseHeader(config.methodHeader)
u.normalizeMethod(method)
###*
Server-side companion libraries like unpoly-rails set this cookie so we
have a way to detect the request method of the initial page load.
There is no JavaScript API for this.
@function up.protocol.initialRequestMethod
@internal
###
initialRequestMethod = u.memoize ->
methodFromServer = up.browser.popCookie(config.methodCookie)
(methodFromServer || 'get').toLowerCase()
# Remove the method cookie as soon as possible.
# Don't wait until the first call to initialRequestMethod(),
# which might be much later.
up.bus.on('up:framework:booted', initialRequestMethod)
###*
Configures strings used in the optional [server protocol](/up.protocol).
@property up.protocol.config
@param {String} [config.targetHeader='X-Up-Target']
@param {String} [config.failTargetHeader='X-Up-Fail-Target']
@param {String} [config.locationHeader='X-Up-Location']
@param {String} [config.titleHeader='X-Up-Title']
@param {String} [config.validateHeader='X-Up-Validate']
@param {String} [config.methodHeader='X-Up-Method']
@param {String} [config.methodCookie='_up_method']
The name of the optional cookie the server can send to
[signal the initial request method](/up.protocol#signaling-the-initial-request-method).
@param {String} [config.methodParam='_method']
The name of the POST parameter when [wrapping HTTP methods](/up.form.config#config.wrapMethods)
in a `POST` request.
@param {String} [config.csrfHeader='X-CSRF-Token']
The name of the HTTP header that will include the
[CSRF token](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Synchronizer_token_pattern)
for AJAX requests.
@param {String|Function} [config.csrfParam]
The `name` of the hidden `` used for sending a
[CSRF token](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Synchronizer_token_pattern) when
submitting a default, non-AJAX form. For AJAX request the token is sent as an HTTP header instead.
The parameter name can be configured as a string or as function that returns the parameter name.
If no name is set, no token will be sent.
Defaults to the `content` attribute of a `` tag named `csrf-token`:
@param {String|Function} [config.csrfToken]
The [CSRF token](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Synchronizer_token_pattern)
to send for unsafe requests. The token will be sent as either a HTTP header (for AJAX requests)
or hidden form `` (for default, non-AJAX form submissions).
The token can either be configured as a string or as function that returns the token.
If no token is set, no token will be sent.
Defaults to the `content` attribute of a `` tag named `csrf-token`:
@experimental
###
config = u.config
targetHeader: 'X-Up-Target'
failTargetHeader: 'X-Up-Fail-Target'
locationHeader: 'X-Up-Location'
validateHeader: 'X-Up-Validate'
titleHeader: 'X-Up-Title'
methodHeader: 'X-Up-Method'
methodCookie: '_up_method'
methodParam: '_method'
csrfParam: -> $('meta[name="csrf-param"]').attr('content')
csrfToken: -> $('meta[name="csrf-token"]').attr('content')
csrfHeader: 'X-CSRF-Token'
csrfParam = ->
u.evalOption(config.csrfParam)
csrfToken = ->
u.evalOption(config.csrfToken)
## Unfortunately we cannot offer reset via event without introducing cycles
## in the asset load order
#
# up.on 'up:framework:reset', reset
reset = ->
config.reset()
config: config
reset: reset
locationFromXhr: locationFromXhr
titleFromXhr: titleFromXhr
methodFromXhr: methodFromXhr
csrfParam :csrfParam
csrfToken: csrfToken
initialRequestMethod: initialRequestMethod
)(jQuery)