Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve content negociation middlewares #150

Merged
merged 31 commits into from
May 13, 2016
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
11b2662
Improve content negociation middlewares
arteymix Dec 22, 2015
166c72f
Provide content negotiation in the 'Valum.ContentNegotiation' namespace
arteymix Jan 12, 2016
23d9d07
Handle 'gzip' and 'deflate' in 'accept_encoding'
arteymix Feb 5, 2016
2d6ae26
Apply a 'CharsetConverter' for 'accept_charset'
arteymix Feb 5, 2016
f0cc06a
Negotiate multiple expectations
arteymix Feb 5, 2016
ae69b01
Preserve the 'Content-Type' parameters in 'accept'
arteymix Feb 5, 2016
42e8b7b
Fix qvalues with multiple expectations for 'negotiate'
arteymix Feb 5, 2016
87b0943
Raise a '406 Not Acceptable' by default in 'negotiate'
arteymix Feb 5, 2016
c9166dc
If no header is present, assume that anything is acceptable
arteymix Feb 5, 2016
18f4003
Convert the charset from the old one in 'accept_charset'
arteymix Feb 5, 2016
90534a5
Tests for 'negotiate' and 'accept'
arteymix Feb 5, 2016
7f5a758
Perform case-insensitive comparison
arteymix Feb 5, 2016
d15b1dd
Documentation for content negotiation
arteymix Feb 5, 2016
199e597
Fix order of arguemnts for 'GLib.CharsetConverter' and typo
arteymix Feb 5, 2016
87b207c
negotiation: Use case-insensitive hash and equal functions
arteymix Feb 7, 2016
08f6cb0
negotiation: Document that unknown encodings are not handled
arteymix Feb 25, 2016
55a281c
Generate coverage from Codecov
arteymix Feb 25, 2016
fd8f0e8
Improve 'accept_charset' middleware
arteymix Feb 29, 2016
04b93c3
negotiation: Use a quality list for expectations
arteymix May 10, 2016
30ca640
negotiation: Remove 'forward' callback and its default usage
arteymix May 11, 2016
7b66ecd
negotiation: Negotiate the best quality and preference product
arteymix May 12, 2016
15676a4
negotiation: Remove charset conversion
arteymix May 12, 2016
9d86979
negotiation: Include 'x-gzip' when negotiating a 'Accept-Encoding'
arteymix May 12, 2016
3963c0a
Merge branch 'negociation-middlewares' into 'master'
arteymix May 12, 2016
b31b66d
vsgi: Set 'Content-Length' only if it can be determined reliably for …
arteymix May 12, 2016
60d0ba2
negotiation: Remove 'NegotiateFlags.NEXT'
arteymix May 12, 2016
3274f64
negotiation: Update the documentation
arteymix May 12, 2016
aace9e2
negotiation: Support vendor prefix 'x-' for 'accept_encoding'
arteymix May 12, 2016
4e279fe
negotiation: Fix qvalue extraction end offset
arteymix May 12, 2016
a9fad67
negotiation: Make global variant acceptable for regional preference
arteymix May 13, 2016
426368e
negotiation: Full coverage for tests
arteymix May 13, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions docs/middlewares/content-negotiation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
Content Negotiation
===================

Negotiating the resource representation is an essential part of the HTTP
protocol.

The negotiation process is simple: expectations are provided for a specific
header, if they are met, the processing is forwarded with the highest quality
value, otherwise a ``406 Not Acceptable`` status is raised.

.. code:: vala

using Valum.ContentNegotiation;

app.get ("", negotiate ("Accept",
{"text/html", "text/html+xml"},
(req, res, next, stack, content_type) => {
// produce a response based on 'content_type'
}));

Pass the ``NegotiateFlags.NEXT`` flag to forward with ``next`` instead of
raising a ``406 Not Acceptable``.

.. code:: vala

app.get ("", negotiate ("Accept", {"text/html"}, (req, res) => {
// produce 'text/html'
}, NegotiateFlags.NEXT)).then ((req, res) => {
// the user agent does not accept 'text/html'
});

The default value for the continuation is ``forward``. It will simply call
``next`` to continue to the next middleware following the negotiation. This is
typically what would be found on the top of an application.

.. code:: vala

app.use (negotiate ("Accept", {"text/xhtml"}));

// all the following route assume that 'text/xhtml' is being produced.

app.status (Soup.Status.NOT_ACCEPTABLE, (req, res) => {
// handle '406 Not Acceptable' here
});

A custom comparison function can be provided to ``negotiate`` in order to
handle wildcards and other edge cases. The user agent pattern is the first
argument and the expectation is the second.

.. warning::

Most of the HTTP/1.1 specification about headers is case-insensitive, use
`Soup.str_case_equal`_ to perform comparisons.

.. _Soup.str_case_equal: http://valadoc.org/#!api=libsoup-2.4/Soup.str_case_equal

.. code:: vala

app.use (negotiate ("Accept",
{"text/xhtml"},
forward,
NegotiateFlags.NONE,
(a, b) => {
return a == "*" || Soup.str_case_equal (a, b);
});

Middlewares
-----------

For convenience, middlewares that support edge cases are provided to handle
common headers:

+---------------------+----------------------+------------------------+
| Middleware | Header | Edge cases |
+=====================+======================+========================+
| ``accept`` | ``Content-Type`` | ``*/*`` and ``type/*`` |
+---------------------+----------------------+------------------------+
| ``accept_charset`` | ``Content-Type`` | ``*`` |
+---------------------+----------------------+------------------------+
| ``accept_encoding`` | ``Content-Encoding`` | ``*`` |
+---------------------+----------------------+------------------------+
| ``accept_language`` | ``Content-Language`` | missing language type |
+---------------------+----------------------+------------------------+
| ``accept_ranges`` | ``Content-Ranges`` | none |
+---------------------+----------------------+------------------------+

The ``accept`` middleware will assign the media type and preserve all other
parameters.

The ``accept_encoding`` middleware will convert the :doc:`../vsgi/response` if
it's either ``gzip`` or ``deflate``.

.. warning::

The ``accept_encoding`` middleware must always be applied before
``accept_charset`` to avoid corruption; converting the charset of
a compressed binary format is not an excellent idea.
1 change: 1 addition & 0 deletions docs/middlewares/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ Middlewares
===========

.. toctree::
content-negotiation
server-sent-events
subdomain
18 changes: 12 additions & 6 deletions examples/app/app.vala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

using Valum;
using Valum.ContentNegotiation;
using Valum.ServerSentEvents;
using VSGI.Soup;

Expand Down Expand Up @@ -281,16 +282,21 @@ app.get ("server-sent-events", stream_events ((req, send) => {
});
}));

app.get ("negociate", accept ("application/json", (req, res) => {
app.get ("negotiate", accept ({"application/json"}, (req, res) => {
res.headers.set_content_type ("application/json", null);
res.body.write_all ("{\"a\":\"b\"}".data, null);
})).then (accept ("text/xml", (req, res) => {
}, NegotiateFlags.NEXT)).then (accept ({"text/xml"}, (req, res) => {
res.headers.set_content_type ("text/xml", null);
res.body.write_all ("<a>b</a>".data, null);
})).then ((req, res) => {
res.status = global::Soup.Status.NOT_ACCEPTABLE;
res.body.write_all ("Supply the 'Accept' header with either 'application/json' or 'text/xml'.".data, null);
});
}));

app.get ("negotiate-charset", accept_charset ({"iso-8859-1"}, (req, res) => {
res.body.write_all ("Héllo world!".data, null);
}));

app.get ("negotiate-encoding", accept_encoding ({"gzip", "deflate"}, (req, res, next, stack, encoding) => {
res.body.write_all ("Hello world! (compressed with %s)".printf (encoding).data, null);
}));

app.status (Soup.Status.NOT_FOUND, (req, res, next, stack) => {
var template = new View.from_stream (resources_open_stream ("/templates/404.html", ResourceLookupFlags.NONE));
Expand Down
10 changes: 9 additions & 1 deletion examples/app/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,15 @@ <h4>Extras</h4>
<ul class="list-inline">
<li><a href="/ctpl/foo/bar">CTPL templating</a></li>
<li><a href="/server-sent-events">Server-Sent Events</a></li>
<li><a href="/negociate">negociate</a></li>
<li>
<a href="/negotiate">negotiate</a>
</li>
<li>
<a href="/negotiate-encoding"> negotiate encoding (gzip or deflate)</a>
</li>
<li>
<a href="/negotiate-charset">negotiate charset (iso-8859-1)</a>
</li>
</ul>
<footer>
<p class="text-center">
Expand Down
Loading