http and your angry dog
DESCRIPTION
As presented at ZendCon, Confoo, LaraconEU, ZgPHP, PFCongres and Fronteers User Group. An overview of some intermediate level HTTP features and how they might be useful in practice.TRANSCRIPT
HTTP
ZendCon 2014
& Your Angry Dog
Ross Tuck
Freerange CodemonkeyKnow-It-All
Hot-Air Balloon
Today's topic:
Dogs
HTTP & Dogs
The Agenda
Basics
Client Server
Request
Response
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request
2 Parts
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request
The body
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>My application</title>
...
Request
The body
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request
The body
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request
The headers
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request
The good stuf
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request
GET, POST, PUT, DELETE
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request Relative URLHTTP version
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request
Key/Value pairs
POST /gists HTTP/1.1
Authorization: Basic xxxxxxxx
Host: api.github.com
Content-Length: 146
{
"description": "the description for this gist",
"public": false,
"files": {
...
Request
HTTP/1.1 201 Created
Date: Sun, 09 Sep 2012 11:42:41 GMT
Content-Length: 1848
Location: https://api.github.com/gists/a43a0cf58
{
"description": "the description for this gist",
"comments": 0,
"created_at": "2012-09-09T11:42:40Z",
...
ResponseStatus code
• 2xx
• 3xx
• 4xx
• 5xx
OK!
Over there!
Client screwed up!
Server screwed up!
Content Negotiation
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Request
HTTP/1.1 200 OK
Date: Sun, 26 Aug 2012 18:00:43 GMT
{
"cute": true,
"big": false,
"data_dog": true
}
Response
HTTP/1.1 200 OK
Date: Sun, 26 Aug 2012 18:00:43 GMT
<dog breed="corgi">
<cute>true</cute>
<data capacity="on" />
</dog>
Response
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Request
GET /dogs/corgi.json HTTP/1.1
Host: api.example.com
Request
/dogs/corgi.json !== /dogs/corgi.xml
Imagine the URL as your primary key.
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Request
GET /dogs/corgi?_format=json HTTP/1.1
Host: api.example.com
Request
POST /dogs/corgi?_format=json HTTP/1.1
Host: api.example.com
Request
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Request
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Accept: application/json
Request
More POWAH
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Accept: application/json
Request
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Accept: application/json, application/xml
Request
How do I choose?
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Accept: application/json, application/xml
Request
HTTP/1.1 200 OK
Date: Sun, 26 Aug 2012 18:00:43 GMT
Content-Type: application/json
{
"cute": true,
"big": false,
"data_dog": true
}
Response
Nifty.
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Accept: application/json, application/xml
Request
text/html, text/plain
text/html;key=value, text/plain
text/html;key=value;foo=bar, text/plain
text/html, text/plain
text/html, text/plain;q=0.5
Quality(Default 1.0)
text/html, text/plain;q=0.5, text/*;q=0.1
Wildcards
text/html, text/plain;q=0.5, */*;q=0.1
Anything at all
Accept HeadersLittle weird...
But not so scary.
text/html,application/xhtml+xml,
application/xml;q=0.9,*/*;q=0.8
Cool...
What the heck is it good for?
Accept is a “Pattern”
Accept-LanguageAccept-EncodingAccept-CharsetAccept-Ranges
Content-LanguageContent-Encoding(works differently)
Content-Range
Resource vs Representation
/dog/corgi
JSON, Dutch,Gzipped,/dog/corgi
Resource vs Representation
Best way to version your API.
Arguably.Right now.
/v1/dogs/corgi
Accept: application/vnd.dogipedia-v1+json
Accept: application/vnd.dogipedia-v2+json
Vary
Client Server
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Client Server
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Client ServerProxy
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Client ServerProxy
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Accept: application/json, text/plain
User-Species: cat
Same URL.Different output.
WTF should I return?
Client ServerProxy
GET /dogs/corgi HTTP/1.1
Host: api.example.com
Accept: application/json, text/plain
User-Species: cat
Client ServerProxy
Here's how.
Hint:Involves theVary header!
Client ServerProxy
/dogs/corgiAccept: application/json, text/plainUser-Species: cat
HTTP/1.1 200 OK
Date: Sun, 26 Aug 2012 18:00:43 GMT
Content-Type: application/json
Vary: Accept
{“json”: “omgz”}
Response
Client ServerProxy
URL and Accept?Okay, I got this.
Some time later...
Client ServerProxy
/dogs/corgiAccept: application/json, text/plainUser-Species: aardvark
Client ServerProxy
Valid cache.I has it.
Client ServerProxy
ZZ ZZZ
Z
HTTP/1.1 200 OK
Date: Sun, 26 Aug 2012 18:00:43 GMT
Content-Type: application/json
Vary: Accept
{“json”: “omgz”}
Response
HTTP/1.1 200 OK
Date: Sun, 26 Aug 2012 18:00:43 GMT
Content-Type: application/json
Vary: Accept, User-Species
{“json”: “omgz”}
Response
HTTP/1.1 200 OK
Date: Sun, 26 Aug 2012 18:00:43 GMT
Content-Type: application/json
Vary: Accept, User-Species
{“json”: “dogs rule, cats drool”}
Response
Request headers.Not Response!
Bad Reputation?2 Reasons
1. Accept-Encoding -Language
2. Internet Explorer
Caching
ExpiresPragma
Cache-Control
ExpiresPragma
Cache-Control
Expires
Cache-Control
HTTP 1.0
HTTP 1.1
HTTP/1.1 200 OK
Expires: Wed, 29 Oct 2014 22:00:00 GMT
{“herp”: “derp”}
Response
HTTP/1.1 200 OK
Cache-Control: max-age=120
{“herp”: “derp”}
Response
HTTP/1.1 200 OK
Cache-Control: max-age=120
Response
Expires
Cache-Control
HTTP 1.0
HTTP 1.1
HTTP/1.1 200 OK
Cache-Control: max-age=120
{“herp”: “derp”}
Response
HTTP/1.1 200 OK
Cache-Control: max-age=120, s-maxage=120
{“herp”: “derp”}
Response
Dude, Where's my dash?
publicprivate
no-storeno-cache
no-transform
must-revalidateproxy-revalidate
Mark Nottingham's Caching Tutorialhttp://www.mnot.net/cache_docs/
Much better than me.
Conditional Requests
Conditional Requests
The Part About ETags
Conditional Requests
DELETE /ross/reputation HTTP/1.1
Host: api.joind.in
If-Talk-Quality: Crap
Request
if ($talkQuality === 'Crap') {
delete($rossReputation);
}
Not real code
Server
What kind of conditions?
If-MatchIf-None-Match
If-Modified-SinceIf-Unmodified-Since
If-Range
ETags
Datetimes
Either
Wait a second, Ross.
Audience
What the heck is an ETag?
A string.Any string.
One rule:
Represent the current state.
“14”“a381bedb5d4478053eb04be35f8798dd”
“winnie-the-pooh”
...for the current representation.
etag(“v14-json-en”) !== etag(“v14-xml-en”)
Don't cross the streams
Server
Last Modified Date sounds easier...
Audience
Wed, 15 Nov 1995 04:58:08 GMT
One second of precision
Caching With Conditionals
Use Case
GET /gists/3481910 HTTP/1.1
Host: api.github.com
Accept: */*
Request
HTTP/1.1 200 OK
Server: nginx/1.0.13
Date: Sun, 26 Aug 2012 18:00:43 GMT
Vary: Accept
ETag: "f4e15911542b92b44bb38186e71cc8f5"
"history": [
{
"version": "529f6311d5518977534b6e1fd313...",
...
Response
...
"user": {
"gravatar_id": "c26bfcbd5f786591e036fa0",
"avatar_url": "https://secure.gravatar...",
"login": "rosstuck",
"url": "https://api.github.com/users/rosstuck",
"id": 146766
},
"change_status": {
"additions": 1,
"deletions": 0,
"total": 1
},
Response
"url": "https://api.github.com/gists/348...",
"committed_at": "2012-08-26T17:40:03Z"
}
],
"git_pull_url": "git://gist.github.com/34819...",
"forks": [
],
"html_url": "https://gist.github.com/3481910",
"git_push_url": "[email protected]:3481910.git",
"comments": 0,
"user": {
Response
HTTP/1.1 200 OK
Server: nginx/1.0.13
Date: Sun, 26 Aug 2012 18:00:43 GMT
Vary: Accept
ETag: "f4e15911542b92b44bb38186e71cc8f5"
{
"history": [
{
"version": "529f6311d5518970903cb5427534b6e1fd313aca",
"user": {
"gravatar_id": "c26bfcbd5f786591e036fa0958a11e8b",
"avatar_url": "https://secure.gravatar.com/avatar/c26bfcbd5f786591e036fa0958a11e8b?d=https://a2...
"login": "rosstuck",
"url": "https://api.github.com/users/rosstuck",
"id": 146766
},
"change_status": {
"additions": 1,
"deletions": 0,
"total": 1
},
"url": "https://api.github.com/gists/3481910/529f6311d5518970903cb5427534b6e1fd313aca",
"committed_at": "2012-08-26T17:40:03Z"
}
],
"git_pull_url": "git://gist.github.com/3481910.git",
Response
"forks": [
],
"html_url": "https://gist.github.com/3481910",
"git_push_url": "[email protected]:3481910.git",
"comments": 0,
"user": {
"gravatar_id": "c26bfcbd5f786591e036fa0958a11e8b",
"avatar_url": "https://secure.gravatar.com/avatar/c26bfcbd5f78659....",}
"login": "rosstuck",
"url": "https://api.github.com/users/rosstuck",
"id": 146766
},
"public": true,
"created_at": "2012-08-26T17:40:03Z",
"files": {
"gistfile1.txt": {
"type": "text/plain",
"filename": "gistfile1.txt",
"raw_url": "https://gist.github.com/raw/3481910/8b6946739e8098408ee3af96...
"content": "Hello PFC!",
"language": null,
"size": 10
}
},
"description": "",
"url": "https://api.github.com/gists/3481910",
"updated_at": "2012-08-26T17:40:03Z",
"id": "3481910"
}
GET /gists/3481910 HTTP/1.1
Host: api.github.com
Accept: */*
If-None-Match: "f4e15911542b92b44bb38186e71cc8f5"
Request
HTTP/1.1 304 Not Modified
Server: nginx/1.0.13
Date: Sun, 26 Aug 2012 18:00:43 GMT
Vary: Accept
ETag: "f4e15911542b92b44bb38186e71cc8f5"
Response
HTTP/1.1 304 Not Modified
Server: nginx/1.0.13
Date: Sun, 26 Aug 2012 18:00:43 GMT
Vary: Accept
ETag: "f4e15911542b92b44bb38186e71cc8f5"
Response
HTTP/1.1 304 Not Modified
Server: nginx/1.0.13
Date: Sun, 26 Aug 2012 18:00:43 GMT
Vary: Accept
ETag: "f4e15911542b92b44bb38186e71cc8f5"
Response
No giant body!
Caching.You has it.
GET /gists/3481910 HTTP/1.1
Host: api.github.com
Accept: */*
If-None-Match: "a381bedb5d4478053eb04be35f8798dd"
Request
GET /gists/3481910 HTTP/1.1
Host: api.github.com
Accept: */*
If-None-Match: "ross-is-a-poo-poo-head"
Request
HTTP/1.1 200 OK
Server: nginx/1.0.13
Date: Sun, 26 Aug 2012 18:00:43 GMT
Vary: Accept
ETag: "f4e15911542b92b44bb38186e71cc8f5"
"history": [
{
"version": "529f6311d5518977534b6e1fd313...",
Response
Recap
No ETag Old ETag Matching ETag
Full BodyFull BodyNo Body
→
→
→
...on supported servers.
Why?
ParsingBandwidth
Response time
Probably
.Maybe
However...
“The fastest request is one you don't make.”
- Jesus
More Fun With ETags
Optimistic Concurrency Control
“Record Versioning”
Request
GET /gists/3481910 HTTP/1.1
Host: api.github.com
Accept: */*
If-None-Match: "f4e15911542b92b44bb38186e71cc8f5"
Request
PATCH /gists/3481910 HTTP/1.1
Host: api.github.com
Accept: */*
If-None-Match: "f4e15911542b92b44bb38186e71cc8f5"
Request
PATCH /gists/3481910 HTTP/1.1
Host: api.github.com
Accept: */*
If-Match: "f4e15911542b92b44bb38186e71cc8f5"
Request
PATCH /gists/3481910 HTTP/1.1
Host: api.github.com
Accept: */*
If-Match: "f4e15911542b92b44bb38186e71cc8f5"
{ "description": "cheese om nom nom" }
Request
Response
Response
HTTP/1.1 200 OK
Server: nginx/1.0.13
Date: Sat, 01 Sep 2012 14:01:38 GMT
ETag: "899b76047a5e68445668374c2e0faa32"
{
"description": "cheese om nom nom",
"user": {
"login": "rosstuck",
...
It works.
So what?
What if I send something...
PATCH /gists/3481910 HTTP/1.1
Host: api.github.com
Accept: */*
If-Match: "899b76047a5e68445668374c2e0faa32"
{ "description": "cheese om nom nom" }
Request
PATCH /gists/3481910 HTTP/1.1
Host: api.github.com
Accept: */*
If-Match: "stay-puft-marshmellow-dog!"
{ "description": "cheese om nom nom" }
Request
HTTP/1.1 412 Precondition Failed
Server: nginx/1.0.13
Date: Sun, 26 Aug 2012 18:00:43 GMT
Response
Response
if (“stay-puft-marshmellow-dog” == “f4e1591..”) {
patchTheRecord();
}
Server
if (“stay-puft-marshmellow-dog” == “f4e1591..”) {
patchTheRecord();
} else {
sendScary412Message();
}
Server
Your ETag is out of date.
“Two guys on the same record” problem
Scary Precondition Error pt. 2
DisclaimerNew Stuff Ahead
DELETE /gists/3481910 HTTP/1.1
Host: api.github.com
Request
HTTP/1.1 428 Precondition Required
Server: nginx/1.0.13
Date: Sun, 26 Aug 2012 18:00:43 GMT
Response
Am I operating on the latest version?
DELETE /gists/3481910 HTTP/1.1
Host: api.github.com
Request
DELETE /gists/3481910 HTTP/1.1
Host: api.github.com
If-Match: "f4e15911542b92b44bb38186e71cc8f5"
Request
HTTP/1.1 204 No Content
Server: nginx/1.0.13
Date: Sun, 26 Aug 2012 18:00:43 GMT
Response
Look before you leap.
Tooling
Epilogue: HTTP & Dogs
Content NegotiationVary
CachingPreconditions
Treat it like your framework.
Questions?
Resources
• RFC 7231–Sections 4 - 7
• http://redbot.org/
• http://mnot.net/cache_docs/
• http://charlesproxy.com/
• http://guzzlephp.org/
Image Credits• http://www.sxc.hu/photo/555539• http://www.flickr.com/photos/thedalogs/2953136078/• http://www.sxc.hu/photo/678952• http://www.flickr.com/photos/designgate/8317884432/• http://www.flickr.com/photos/barretthall/3289317664/• http://www.flickr.com/photos/binaryape/3702275400/lightbox/• http://www.flickr.com/photos/istolethetv/2956799679/in/photostream/• http://theflashbackspodcast.blogspot.nl/2012/01/30-day-film-challenge-day-8.html• http://www.flickr.com/photos/jonomueller/6906420190/• http://www.flickr.com/photos/cookbookman/6175752147• http://www.flickr.com/photos/creativedc/2913123330/• http://www.flickr.com/photos/epc/44053757/• http://www.flickr.com/photos/soggydan/4698849104/• http://www.flickr.com/photos/owldreams/4430175427/• http://www.flickr.com/photos/piddleville/2499539542/• http://www.flickr.com/photos/danja/5665671907/• http://www.flickr.com/photos/cradlehall/3574160744/• http://www.flickr.com/photos/ironypoisoning/7114801437/• http://www.flickr.com/photos/piddleville/2499539542/lightbox/• http://everydayidrawadog.blogspot.nl/2009_02_01_archive.html
joind.in/12071
@rosstuck
Ross Tuckrosstuck.com