talking heads - writing an api does not need to be a "psycho killer"
DESCRIPTION
In a world where things get more and more connected to services there is a dire need to understand the principals off HTTP protocols. It is simply not enough to 'just return' some data and expect the the client knows what to do. Proper clients, wether it is a 'thing' or 'software' can intelligently communicate with servers. And servers should take the responsibility to give clear answers.... or let the client know that it is not fully clear what the client requested. This talk is a brief overview on how client and server communicate with each other concerning caching, content-negociation and the methods provided. Its all about what is going on inside the HEADs when client and server are TALKING http. Then there is Web::Machine, based on Basho's work. It brings structure in the whole decision tree of what is happening with all the Request Header Fileds. A nice work from, but arguably, it has some weak spots. And of course a great new module for Dancer2 will be introduced: Dancer2::Plugin::HTTP::Auth::Extensible, the first of a series that will make life easy when developing REST api's with DancerTRANSCRIPT
Talking HeadsREST api’s don’t need to be your “psycho-killer”
Dancer 2A Lightweight WEB Framework
… that does nothing wrong
Dancer 2A Lightweight WEB Framework
… that does nothing you don’t want
Dancer 2A Lightweight WEB Framework … does it do what it should do?
RESTful API’s Dancer 2
HTTP protocols and RFC’s
Overview• HTTP 1.1 Protocol
• Let’s Dance
• Caching
• Conditional Requests
• Content Negotiation
• Authorization
HTTP/1.1 Protocol
• Requests
• Responses
• Request Methods
• Header Fields
• Status codes
HTTP 1.1 Protocol Request
GET /some_resource HTTP/1.1 Host: server.tst User-Agent: insane_client/0.1 Accept: text/html; q=0.8, image/gif; q=0.3 Accept-Charset: iso8509-1 Content-Length: 123 Here can come some content, for what ever purpose
HTTP 1.1 Protocol Request
GET /some_resource HTTP/1.1 Host: server.tst User-Agent: insane_client/0.1 Accept: text/html; q=0.8, image/gif; q=0.3 Accept-Charset: iso8509-1 Content-Length: 123 Here can come some content, for what ever purpose
HTTP 1.1 Protocol Request
GET /some_resource HTTP/1.1 Host: server.tst User-Agent: insane_client/0.1 Accept: text/html; q=0.8, image/gif; q=0.3 Accept-Charset: iso8509-1 Content-Length: 123 Here can come some content, for what ever purpose
HTTP 1.1 Protocol Request
GET /some_resource HTTP/1.1 Host: server.tst User-Agent: insane_client/0.1 Accept: text/html; q=0.8, image/gif; q=0.3 Accept-Charset: iso8509-1 Content-Length: 123 Here can come some content, for what ever purpose
HTTP 1.1 Protocol Request
GET /some_resource HTTP/1.1 Host: server.tst User-Agent: insane_client/0.1 Accept: text/html; q=0.8, image/gif; q=0.3 Accept-Charset: iso8509-1 Content-Length: 123 Here can come some content, for what ever purpose
HTTP 1.1 Protocol Response
HTTP/1.1 200 OK Server: dancer/2.0 Date: Tue, 14 Oct 2014 12:34:56 GMT Content-Type: text/plain Charset: iso8509-1 Content-Length: 456 You asked for something, here it is!
HTTP 1.1 Protocol Response
HTTP/1.1 200 OK Server: dancer/2.0 Date: Tue, 14 Oct 2014 12:34:56 GMT Content-Type: text/plain Charset: iso8509-1 Content-Length: 456 You asked for something, here it is!
HTTP 1.1 Protocol Response
HTTP/1.1 200 OK Server: dancer/2.0 Date: Tue, 14 Oct 2014 12:34:56 GMT Content-Type: text/plain Charset: iso8509-1 Content-Length: 456 You asked for something, here it is!
HTTP 1.1 Protocol Response
HTTP/1.1 200 OK Server: dancer/2.0 Date: Tue, 14 Oct 2014 12:34:56 GMT Content-Type: text/plain Charset: iso8509-1 Content-Length: 456 You asked for something, here it is!
HTTP 1.1 Protocol Response
HTTP/1.1 200 OK Server: dancer/2.0 Date: Tue, 14 Oct 2014 12:34:56 GMT Content-Type: text/plain Charset: iso8509-1 Content-Length: 456 You asked for something, here it is!
HTTP 1.1 Protocol Request Methodes
• GET
• POST
• PUT
• DELETE
• HEAD
• OPTIONS
• PATCH
HTTP 1.1 Protocol Header Fields
• Host
• User-Agent
• If-Modified-Since
• Content-Type
• Content-Length
• Location
• Server
• Last-Modification
• Content-Type
• Content-Length
HTTP 1.1 Protocol Status Codes
• 1—— Informational
• 2—— Succes
• 3—— Redirection
• 4—— Client Error
• 5—— Server
Status: 404Not Found
Status: 404Not Found
Status: 500Internal Server Error
HTTP 1.1 Protocol Status 1—— Informational
• 100 Continue
HTTP 1.1 Protocol Status 2—— Succes
• 200 OK
• 201 Created
• 202 Accepted
• 204 No Content
HTTP 1.1 Protocol Status 3—— Redirection
• 300 Multiple Choices
• 301 Moved Permanently
• 304 Not Modified
HTTP 1.1 Protocol Status 4—— Client Error
• 400 Bad Request
• 401 Unauthorized
• 403 Forbidden
• 404 Not Found
• 405 Method Not Allowed
• 407 Not Acceptable
• 410 Gone
• 412 Precondition Failed
HTTP 1.1 Protocol Status 5—— Server Error
• 500 Internal Server Error
• 501 Not Implemented
• 503 Service Unavailable
It takes Two
Dancer 2 & LWP
Client Server Let’s Dance
use Dancer2; get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); template ’user.tt’, $user_info; }; dance; 1;
Client Server Let’s Dance
use Dancer2; get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); template ’user.tt’, $user_info; }; dance; 1;
Client Server Let’s Dance
use Dancer2; get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); template ’user.tt’, $user_info; }; dance; 1;
Client Server Let’s Dance
use Dancer2; get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); template ’user.tt’, $user_info; }; dance; 1;
Client Server Let’s Dance
use Dancer2; get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); template ’user.tt’, $user_info; }; dance; 1;
Client Server Let’s Dance
use Dancer2; get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); template ’user.tt’, $user_info; }; dance; 1;
Client Server LWP::UserAgent
use LWP; use LWP::UserAgent; my $agent = LWP::UserAgent->new; my $response = $agent->get(’/user/999-999’);
Caching Temporary Storage
• Reduce cost
• Improve Responsivness
Caching Temporary Storage
• Client Side
• Public Proxy
• Server Side
Caching If-Modified-Since
use LWP; use LWP::UserAgent; my $agent = LWP::UserAgent->new; my $request = HTTP::Request->new( GET => ’/user/999-999-999’); $request->header( If-Modified-Since => Tue, 14 Oct 2014 12:34:56 GMT ); my $response = $agent->request($request);
Caching Response
• Response Status: 304 Not Modified
• Last-Modified: Tue, 07 Oct 2014 12:34:56 GMT
• Age: 3600
• Expire: Tue, 21 Oct 2014 12:34:56 GMT
• Cache-Control: public | private | max-age | no-cache
Conditional Requests
• Stateless
• No Resource Locking
• Only execute request if not modified since last
• Only execute request if the resource is still the same
Conditional Requests If-Unmodified-Since
use LWP; use LWP::UserAgent; my $agent = LWP::UserAgent->new; my $request = HTTP::Request->new( DELETE => ’/user/999-999-999’); $request->header( If-Unmodified-Since => ’Tue, 14 Oct 2014 12:34:56 GMT’); my $response = $agent->request($request);
Conditional Requests If-Match
use LWP; use LWP::UserAgent; my $agent = LWP::UserAgent->new; my $request = HTTP::Request->new( DELETE => ’/user/999-999-999’); $request->header( If-Match => ’a23dfe4532dab7b21a83d3e0f4c2a6f1’ ); my $response = $agent->request($request);
Conditional Requests Response
• Response Status: 412 Precondition Failed
• Response Status: 428 Precondition Required
• Last-Modified: Tue, 07 Oct 2014 12:34:56 GMT
• ETag: a23dfe4532dab7b21a83d3e0f4c2a6f1
Content Negotiation
• the same resource
• another representation
• the same URL
• client can let the server know what type is preferred
• server will try to deliver in requested content
• caches need to know that this is another variant
• the same URL
Content Negotiation Resources & Representation• Uniform Resouse Identifier
• Does not say anything about representation:
• Charset
• Encoding
• Language
• Format
Content Negotiation Accept & friends
• Accept: text/html, text/plain, image/png
• Accept-Language: nl, en, fr
• Accept-Charset: iso_
• Accept-Encoding: gzip
Content Negotiation Accepting Preferences
• The client can have some nice preferences
• it might like some representation above the other
• it might not like anything else
• The server can only deliver some representations
• it might deliver something it prefers
• it might give a list of options
Content Negotiation Mutable Serializer
use Dancer2; use Dancer2::Plugin::Mutable::Serializer;get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); return $user_info; }; 1;
Content Negotiation Mutable Serializer
use LWP; use LWP::UserAgent; my $agent = LWP::UserAgent->new; my $request = HTTP::Request->new( GET => ’/user/999-999-999’); $request->header( Accept => ’application/json’ ); my $response = $agent->request($request);
Content Negotiation Mutable Serializer
use LWP; use LWP::UserAgent; my $agent = LWP::UserAgent->new; my $request = HTTP::Request->new( GET => ’/user/999-999-999’); $request->header( Accept => ’application/json’ ); $request->header( Content-Type => ’application/xml’ ); my $response = $agent->request($request);
Content Negotiation Let’s do things differently
use Dancer2; get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); if (grep $_ eq ’text/html’, header(’Accept’)) { template ’user.tt’, $user_info; } if (grep $_ eq ’application/json’, header(’Accept’’)) { return to_json $user_info; } if (grep $_ eq ’application/xml’, header(’Accept’’)) { return to_xml $user_info; } }; 1;
Content Negotiation Let’s do things differently
use LWP; use LWP::UserAgent; my $agent = LWP::UserAgent->new; my $request = HTTP::Request->new( GET => ’/user/999-999-999’); $request->header( Accept => ’application/json’ ); my $response = $agent->request($request);
Content Negotiation Let’s do things differently
use Dancer2; get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); if (grep $_ eq ’text/html’, header(’Accept’)) { template ’user.tt’, $user_info; } if (grep $_ eq ’application/json’, header(’Accept’’)) { return to_json $user_info; } if (grep $_ eq ’application/xml’, header(’Accept’’)) { return to_xml $user_info; } }; 1;
Content Negotiation Let’s do things differently
use LWP; use LWP::UserAgent; my $agent = LWP::UserAgent->new; my $request = HTTP::Request->new( GET => ’/user/999-999-999’); $request->header( Accept => ’application/xml; q=0.1, text/html’ ); my $response = $agent->request($request);
Content Negotiation Let’s do things differently
use Dancer2; get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); if (grep $_ eq ’text/html’, header(’Accept’)) { template ’user.tt’, $user_info; } if (grep $_ eq ’application/json’, header(’Accept’’)) { return to_json $user_info; } if (grep $_ eq ’application/xml’, header(’Accept’’)) { return to_xml $user_info; } }; 1;
Content Negotiation Let’s get Messy
Accept: text/plain; q=0.3, text/html; q=0.5, */*; q=0.0
text/plainq=0.3
text/htmlq=0.5
*/*q=0.0
«I’m fine with plain-txt, but like html more… but if it’s anything else, I DON’T WANT THAT»
Content Negotiation Let’s get Messy
use Dancer2; use Dancer2::Plugin::HTTP::ContentNegotiation;get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); http_choose_accept ( ’text/html’ => sub {template ’user.tt’, $user_info}, ’application/json’ => sub {to_json $user_info}, ’application/xml’ => sub {to_xml $user_info}, ); };
Content Negotiation Let’s get Messy
use Dancer2; use Dancer2::Plugin::HTTP::ContentNegotiation;get ’/user/:id’ => { my $user_info = resultset(’users’)->find(id=>params(’id’)); http_choose_accept ( [’image/png’, ’image/jpg’, ’image/gif’] => sub {magick(http_accept->minor)}, ); };
Content Negotiation Resources & Representation• Status 300: Multiple Choices
• format at the end of the URL
• language at the beginning of the URL
• Status 406: Not Acceptable
• Vary: Accept, Aceept-Language . . .
Auth Resource Access
• Stateless
• Submit credentials with every request
• Authentication
• Username & Password
• Authentication Scheme
• Authorisation
• Rolebased access
Auth Usual WEB handling
1. Attempt to acces some page
2. Not Authorised ?
3. Go to /login
4. Send credentials
5. Authenticated Now ?
6. Setup session cookie
7. Go back to original requested page
Auth REST api
1. Attempt to acces some page
2. Not Authenticated ?
3. Status: 401 “Not Authorized”
4. Resend same request including credentials
5. Authorised ?
• Continue processing
• Status: 403 “Forbidden”
Auth HTTP Auth::Extensible
use Dancer2; use Dancer2::Plugin::HTTP::Auth::Extensible; get '/realm' => http_require_authentication sub { "You are logged in using realm: " . http_realm }; get '/vodka' => http_require_role HardDrinker => sub { "Only hard drinkers get vodka"; };
Auth HTTP Auth::Extensible
use Dancer2; use Dancer2::Plugin::HTTP::Auth::Extensible; get '/realm' => http_require_authentication sub { "You are logged in using realm: " . http_realm }; get '/vodka' => http_require_role HardDrinker => sub { "Only hard drinkers get vodka"; };
Auth HTTP Auth::Extensible
use Dancer2; use Dancer2::Plugin::HTTP::Auth::Extensible; get '/realm' => http_require_authentication sub { "You are logged in using realm: " . http_realm }; get '/vodka' => http_require_role HardDrinker => sub { "Only hard drinkers get vodka"; };
Auth HTTP Auth::Extensible
plugins: 'HTTP::Auth::Extensible': realms: example: provider: Config users: - user: ‘beerdrinker' pass: ‘password' name: 'Beer drinker’ roles: - BeerDrinker
Auth Resource Access
• Status 401: Unauthorized
• You should return a WWW-Authenticate also
• HTTP Request Header Field: Authorize
• Status 403: Forbidden
Dancer2::Plugin::HTTP Family
• Dancer2::Plugin::HTTP::ContentNegotiation
• Dancer2::Plugin::HTTP::Auth::Extensible
• Dancer2::Plugin::HTTP::Conditional
• Dancer2::Plugin::HTTP::Cache
Dancer2::Plugin::HTTP HTTP::Header::ActionPack
• Authentication / Authorisation
• MIME-types
• DateTime conversion
Dancer2::Plugin::HTTP HTTP::Header::ActionPack
• Authentication / Authorisation
• MIME-types
• DateTime conversion
Net::WebMachine HTTP::Header::ActionPack
• Basho schema
• Ruby implementation
• Stevan Little / Dave Rolsky