This commit is contained in:
HoneyryderChuck 2019-05-24 14:10:31 +00:00
parent 4e3ff764a3
commit 26efb9daea
47 changed files with 1638 additions and 109 deletions

1
.gitignore vendored
View File

@ -15,3 +15,4 @@ tmp
www/public
www/build
www/.sass-cache
www/wiki

View File

@ -10,8 +10,8 @@ services:
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .bundle/ruby
- .bundle/jruby
- vendor/ruby
- vendor/jruby
before_script:
- docker info
@ -71,11 +71,6 @@ pages:
- ls public/rdoc
- echo "#"
- ls public/coverage
#- ls public
#- echo "#"
#- ls www/coverage
#- mv www/coverage public/
#- mv www/rdoc/ public/
artifacts:
paths:
- public

View File

@ -1,6 +1,7 @@
SimpleCov.start do
command_name "Minitest"
add_filter "/.bundle/"
add_filter "/vendor/"
add_filter "/test/"
add_filter "/lib/httpx/extensions.rb"
add_filter "/lib/httpx/loggable.rb"

View File

@ -5,7 +5,6 @@ ruby RUBY_VERSION
source "https://rubygems.org"
gemspec
gem "hanna-nouveau", require: false
gem "rake", "~> 12.3"
gem "simplecov", require: false
@ -32,9 +31,9 @@ end
platform :mri_21 do
gem "rbnacl", require: false
gem "rbnacl-libsodium", require: false
end
gem "hanna-nouveau", require: false
gem "faraday", :require => false
gem "pry", :require => false

View File

@ -45,3 +45,24 @@ RDoc::Task.new(:website_rdoc) do |rdoc|
rdoc.options += rdoc_opts
rdoc.rdoc_files.add RDOC_FILES
end
desc "Builds Homepage"
task :prepare_website => ["website_rdoc"] do
require "fileutils"
Dir.chdir "www"
system("bundle install")
FileUtils.rm_rf("wiki")
system("git clone https://gitlab.com/honeyryderchuck/httpx.wiki.git wiki")
Dir.glob("wiki/*.md") do |path|
data = File.read(path)
name = File.basename(path, ".md")
title = name == "home" ? "Wiki" : name.split("-").map(&:capitalize).join(" ")
layout = name == "home" ? "page" : "wiki"
header = "---\n" \
"layout: #{layout}\n" \
"title: #{title}\n" \
"---\n\n"
File.write(path, header + data)
end
end

View File

@ -40,6 +40,7 @@ client.cookies("a" => "b").get(URI) # ... Cookie: "a=b" ...
#also seamlessly integrates with redirect follows
client = HTTPX.plugins(:follow_redirects, :cookies)
response = client.get(URI) #=> ... 200 ...
```
* refactoring: connection pool now thread-local, improves thread-safety;

View File

@ -3,9 +3,11 @@
module HTTPX
module Plugins
#
# This plugin adds a shim #authentication method to the session, which will fill
# This plugin adds a shim +authentication+ method to the session, which will fill
# the HTTP Authorization header.
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Authentication#authentication
#
module Authentication
module InstanceMethods
def authentication(token)

View File

@ -3,11 +3,12 @@
module HTTPX
module Plugins
#
# This plugin adds helper methods to implement HTTP Basic Auth
# https://tools.ietf.org/html/rfc7617
# This plugin adds helper methods to implement HTTP Basic Auth (https://tools.ietf.org/html/rfc7617)
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Authentication#basic-authentication
#
module BasicAuthentication
def self.load_dependencies(klass, *)
def self.load_dependencies(klass)
require "base64"
klass.plugin(:authentication)
end

View File

@ -10,9 +10,11 @@ module HTTPX
#
# It supports both *gzip* and *deflate*.
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Compression
#
module Compression
extend Registry
def self.load_dependencies(klass, *)
def self.load_dependencies(klass)
klass.plugin(:"compression/gzip")
klass.plugin(:"compression/deflate")
end

View File

@ -4,7 +4,7 @@ module HTTPX
module Plugins
module Compression
module Brotli
def self.load_dependencies(klass, *)
def self.load_dependencies(klass)
klass.plugin(:compression)
require "brotli"
end

View File

@ -9,6 +9,8 @@ module HTTPX
#
# It also adds a *#cookies* helper, so that you can pre-fill the cookies of a session.
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Cookies
#
module Cookies
using URIExtensions

View File

@ -3,8 +3,9 @@
module HTTPX
module Plugins
#
# This plugin adds helper methods to implement HTTP Digest Auth
# https://tools.ietf.org/html/rfc7616
# This plugin adds helper methods to implement HTTP Digest Auth (https://tools.ietf.org/html/rfc7616)
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Authentication#authentication
#
module DigestAuthentication
DigestError = Class.new(Error)

View File

@ -11,6 +11,8 @@ module HTTPX
#
# It also doesn't follow insecure redirects (https -> http) by default (see *follow_insecure_redirects*).
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Follow-Redirects
#
module FollowRedirects
MAX_REDIRECTS = 3
REDIRECT_STATUS = (300..399).freeze

View File

@ -3,9 +3,10 @@
module HTTPX
module Plugins
#
# This plugin adds support for upgrading a plaintext HTTP/1.1 connection to HTTP/2.
# This plugin adds support for upgrading a plaintext HTTP/1.1 connection to HTTP/2
# (https://tools.ietf.org/html/rfc7540#section-3.2)
#
# https://tools.ietf.org/html/rfc7540#section-3.2
# https://gitlab.com/honeyryderchuck/httpx/wikis/Follow-Redirects
#
module H2C
def self.load_dependencies(*)

View File

@ -7,6 +7,8 @@ module HTTPX
#
# HTTPX.post(URL, form: form: { image: HTTP::FormData::File.new("path/to/file")})
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Multipart-Uploads
#
module Multipart
module FormTranscoder
module_function

View File

@ -15,9 +15,11 @@ module HTTPX
# This plugin is also not recommendable when connecting to >9000 (like, a lot) different origins.
# So when you use this, make sure that you don't fall into this trap.
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Persistent
#
module Persistent
def self.load_dependencies(klass, *)
klass.plugin(:retries) # TODO: pass default max_retries -> 1 as soon as this is a parameter
def self.load_dependencies(klass)
klass.plugin(:retries, max_retries: 1, retry_change_requests: true)
end
def self.extra_options(options)

View File

@ -14,6 +14,8 @@ module HTTPX
# * Socks4/4a proxies
# * Socks5 proxies
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Proxy
#
module Proxy
Error = Class.new(Error)
PROXY_ERRORS = [TimeoutError, IOError, SystemCallError, Error].freeze
@ -47,7 +49,7 @@ module HTTPX
end
class << self
def configure(klass, *)
def configure(klass)
klass.plugin(:"proxy/http")
klass.plugin(:"proxy/socks4")
klass.plugin(:"proxy/socks5")

View File

@ -6,8 +6,7 @@ module HTTPX
module Plugins
module Proxy
module SSH
def self.load_dependencies(_klass, *)
# klass.plugin(:proxy)
def self.load_dependencies(*)
require "net/ssh/gateway"
end
@ -74,6 +73,21 @@ module HTTPX
end
end
end
module ConnectionMethods
def match?(uri, options)
return super unless @options.proxy
super && @options.proxy == options.proxy
end
# should not coalesce connections here, as the IP is the IP of the proxy
def coalescable?(*)
return super unless @options.proxy
false
end
end
end
end
register_plugin :"proxy/ssh", Proxy::SSH

View File

@ -8,6 +8,8 @@ module HTTPX
# In order to benefit from this, requests are sent one at a time, so that
# no push responses are received after corresponding request has been sent.
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Server-Push
#
module PushPromise
def self.extra_options(options)
options.merge(http2_settings: { settings_enable_push: 1 },

View File

@ -5,6 +5,8 @@ module HTTPX
#
# This plugin adds support for retrying requests when certain errors happen.
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Retries
#
module Retries
MAX_RETRIES = 3
# TODO: pass max_retries in a configure/load block
@ -31,7 +33,7 @@ module HTTPX
end
def_option(:retry_change_requests)
end.new(options)
end.new(options).merge(max_retries: MAX_RETRIES)
end
module InstanceMethods
@ -71,7 +73,7 @@ module HTTPX
def initialize(*args)
super
@retries = @options.max_retries || MAX_RETRIES
@retries = @options.max_retries
end
end
end

View File

@ -203,14 +203,15 @@ module HTTPX
klass.instance_variable_set(:@plugins, @plugins.dup)
end
def plugin(pl, *args, &block)
def plugin(pl, options = nil, &block)
# raise Error, "Cannot add a plugin to a frozen config" if frozen?
pl = Plugins.load_plugin(pl) if pl.is_a?(Symbol)
unless @plugins.include?(pl)
@plugins << pl
pl.load_dependencies(self, *args, &block) if pl.respond_to?(:load_dependencies)
pl.load_dependencies(self, &block) if pl.respond_to?(:load_dependencies)
@default_options = @default_options.dup
@default_options = pl.extra_options(@default_options) if pl.respond_to?(:extra_options)
@default_options = pl.extra_options(@default_options, &block) if pl.respond_to?(:extra_options)
@default_options = @default_options.merge(options) if options
include(pl::InstanceMethods) if defined?(pl::InstanceMethods)
extend(pl::ClassMethods) if defined?(pl::ClassMethods)
@ -227,7 +228,7 @@ module HTTPX
opts.response_body_class.__send__(:include, pl::ResponseBodyMethods) if defined?(pl::ResponseBodyMethods)
opts.response_body_class.extend(pl::ResponseBodyClassMethods) if defined?(pl::ResponseBodyClassMethods)
opts.connection_class.__send__(:include, pl::ConnectionMethods) if defined?(pl::ConnectionMethods)
pl.configure(self, *args, &block) if pl.respond_to?(:configure)
pl.configure(self, &block) if pl.respond_to?(:configure)
@default_options.freeze
end

View File

@ -7,6 +7,7 @@ class UnixTest < Minitest::Test
include HTTPX
def test_unix_session
skip if RUBY_ENGINE == "jruby"
on_unix_server do |path|
session = Session.new(transport: "unix", transport_options: { path: path })
response = session.get("http://unix.com/ping")

View File

@ -1,6 +1,5 @@
# frozen_string_literal: true
require "ostruct"
require_relative "../test_helper"
class SystemResolverTest < Minitest::Test

View File

@ -4,6 +4,8 @@ RUBY_PLATFORM=`ruby -e 'puts RUBY_PLATFORM'`
if [[ "$RUBY_PLATFORM" = "java" ]]; then
apk --update add git bash
elif [[ ${RUBY_VERSION:0:3} = "2.1" ]]; then
apk --update add g++ make git bash libsodium
else
apk --update add g++ make git bash
fi
@ -11,7 +13,7 @@ fi
export PATH=$GEM_HOME/bin:$BUNDLE_PATH/gems/bin:$PATH
mkdir -p "$GEM_HOME" && chmod 777 "$GEM_HOME"
gem install bundler -v="1.17.3" --no-doc --conservative
cd /home && bundle install --jobs 4 --path .bundle && \
cd /home && bundle install --jobs 4 --path vendor && \
bundle exec rake test:ci
RET=$?
@ -23,8 +25,8 @@ if [[ $RET = 0 ]] && [[ ${RUBY_VERSION:0:3} = "2.6" ]]; then
fi
if [[ $RET = 0 ]] && [[ ${RUBY_VERSION:0:3} = "2.6" ]]; then
bundle exec rake website_rdoc && \
cd www && bundle install --jobs 4 --path ../vendor && \
bundle exec rake prepare_website &&
cd www && bundle install --jobs 4 --path ../vendor &&
bundle exec jekyll build -d public
fi

View File

@ -22,14 +22,43 @@ module MinitestExtensions
end
module FirstFailedTestInThread
def self.prepended(*)
super
HTTPX::Session.__send__(:include, SessionExtensions)
end
def setup
super
extend(OnTheFly)
end
module SessionExtensions
def find_connection(request, connections, _)
connection = super
request.instance_variable_set(:@connection, connection)
connection
end
end
def run(*)
(Thread.current[:passed_tests] ||= []) << "#{self.class.name}##{name}"
super
ensure
if !Thread.current[:tests_already_failed] && !failures.empty?
if !skipped? && !Thread.current[:tests_already_failed] && !failures.empty?
Thread.current[:tests_already_failed] = true
puts "first test failed: #{Thread.current[:passed_tests].pop}\n"
puts "this thread also executed: #{Thread.current[:passed_tests].join(", ")}"
puts "this thread also executed: #{Thread.current[:passed_tests].join(", ")}" unless Thread.current[:passed_tests].empty?
end
end
module OnTheFly
def verify_status(response, expect)
if response.is_a?(HTTPX::ErrorResponse) && response.error.message.include?("execution expired")
connection = response.request.instance_variable_get(:@connection)
puts connection.inspect
end
super
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
# patch to make the ssh proxy test work
#
# problem: net-ssh needs the gem `rbnacl` to use ed25519 keys. The gem only requires libsodium
# to be available in the system, however, the last `rbnacl` version that works with ruby 2.1
# requires `rbnacl-libsodium` gem to be installed. And this gem is taking too long to build in
# the alpine docker images, timing out our ruby 2.1 CI tests.
#
# solution: monkey-patch the variable which indicates the lib name to the FFI libsodium bindings.
# the values are the same as in the latest version.
#
if RUBY_VERSION < "2.2" && RUBY_PLATFORM == "x86_64-linux-musl"
RBNACL_LIBSODIUM_GEM_LIB_PATH = ["sodium", "libsodium.so.18", "libsodium.so.23"].freeze
end

View File

@ -2,6 +2,7 @@ source "https://rubygems.org"
gem "jekyll", "~> 3.6.2"
gem "jekyll-gzip", "~> 1.1.0"
gem "jekyll-paginate-v2", "~> 2.0.0"
platform :mri do
gem "jekyll-brotli", "~> 1.0.0"
end

View File

@ -6,15 +6,21 @@ baseurl: /httpx
url: honeyryderchuck.gitlab.io
markdown: kramdown
destination: build
github:
repo: https://gitlab.com/honeyryderchuck/httpx
pagination:
enabled: true
debug: true
per_page: 5
sort_reverse: true
include:
- wiki
exclude:
- Gemfile
- Gemfile.lock
plugins:
- jekyll-gzip
- jekyll-brotli
defaults:
-
scope:
type: pages
values:
layout: default
- jekyll-paginate-v2

View File

@ -1,10 +1,10 @@
-
name: BasicAuthentication
path: BasicAuthentication.html
path: Authentication.html#basic-authentication
description: API and support for Basic Authentication.
-
name: DigestAuthentication
path: DigestAuthentication.html
path: Authentication.html#digest-authentication
description: API and support for Digest Authentication.
-
name: Compression
@ -16,11 +16,15 @@
description: Additional API for setting and interpreting cookies (using `http-cookie`).
-
name: FollowRedirects
path: FollowRedirects.html
path: Follow-Redirects.html
description: Ability to follow redirects.
-
name: Retries
path: Retries.html
description: Ability to retry requests.
-
name: H2C
path: H2C.html
path: H2C-Upgrade.html
description: Upgrade HTTP/1.1 clear-text connections to HTTP/2.
-
name: Proxy
@ -28,7 +32,7 @@
description: Proxy Support (HTTP, HTTPS, Socks 4/4a/5).
-
name: PushPromise
path: PushPromise.html
path: Server-Push.html
description: Support for HTTP/2 Server Push.
-
name: Stream
@ -36,5 +40,9 @@
description: "API and support for Stream (mime-type: `text/event-stream`) responses."
-
name: Multipart Uploads
path: Multipart.html
path: Multipart-Uploads.html
description: "API and support for multipart uploads (using `http-form_data`)."
-
name: Persistent
path: Persistent.html
description: "Persist connections over the lifetime of a process."

View File

@ -1,4 +1,7 @@
-
-
name: "0.4.0"
path: "0_4_0_md.html"
-
name: "0.3.1"
path: "0_3_1_md.html"

29
www/_includes/head.html Normal file
View File

@ -0,0 +1,29 @@
<head>
<link href="http://gmpg.org/xfn/11" rel="profile">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<!-- Enable responsiveness on mobile devices-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1">
<title>
{% if page.title == "Home" %}
{{ site.title }} &middot; {{ site.tagline }}
{% else %}
{{ page.title }} &middot; {{ site.title }}
{% endif %}
</title>
<!-- CSS -->
<link rel="stylesheet" href="{{ '/styles/poole.css' | prepend: site.baseurl }}">
<link rel="stylesheet" href="{{ '/styles/syntax.css' | prepend: site.baseurl }}">
<link rel="stylesheet" href="{{ '/styles/lanyon.css' | prepend: site.baseurl }}">
<link rel="stylesheet" href="{{ '/styles/main.css' | prepend: site.baseurl }}">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=PT+Serif:400,400italic,700%7CPT+Sans:400">
<!-- Icons -->
<link rel="shortcut icon" href="{{ '/images/favicon.ico' | prepend: site.baseurl }}">
<!-- RSS -->
<link rel="alternate" type="application/rss+xml" title="RSS" href="/atom.xml">
</head>

View File

@ -0,0 +1,38 @@
<!-- Target for toggling the sidebar `.sidebar-checkbox` is for regular
styles, `#sidebar-checkbox` for behavior. -->
<input type="checkbox" class="sidebar-checkbox" id="sidebar-checkbox">
<!-- Toggleable sidebar -->
<div class="sidebar" id="sidebar">
<div class="sidebar-item">
<p>{{ site.description }}</p>
</div>
<nav class="sidebar-nav">
<a class="sidebar-nav-item{% if page.url == site.baseurl %} active{% endif %}" href="{{ site.baseurl }}/">Home</a>
{% comment %}
The code below dynamically generates a sidebar nav of pages with
`layout: page` in the front-matter. See readme for usage.
{% endcomment %}
{% assign pages_list = site.pages | sort:"url" %}
{% for node in pages_list %}
{% if node.title != null %}
{% if node.layout == "page" %}
<a class="sidebar-nav-item{% if page.url == node.url %} active{% endif %}" href="{{ node.url }}">{{ node.title }}</a>
{% endif %}
{% endif %}
{% endfor %}
<a class="sidebar-nav-item" href="{{ site.github.repo }}/archive/v{{ site.version }}.zip">Download</a>
<a class="sidebar-nav-item" href="{{ site.github.repo }}">GitHub project</a>
<span class="sidebar-nav-item">Currently v{{ site.data.versions[0][0].name }}</span>
</nav>
<div class="sidebar-item">
<p>
&copy; {{ site.time | date: '%Y' }}. All rights reserved.
</p>
</div>
</div>

View File

@ -1,51 +1,47 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ site.title }}</title>
<meta name="description" content="{{ site.description }}">
<link rel="icon" href="{{ '/images/favicon.ico' | prepend: site.baseurl }}">
<link rel="canonical" href="{{ page.url | remove: 'index.html' | prepend: site.baseurl | prepend: site.url }}">
<link rel="stylesheet" href="{{ '/styles/main.css' | prepend: site.baseurl }}">
<!DOCTYPE html>
<html lang="en-us">
<style>
.ribbon {
/* positioning */
position: fixed;
padding: 6px 41px;
width: 150px;
/* top right of the page */
top: 25px;
right: -40px;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
/* effects with some shadow */
box-shadow: 0 0 0 3px #0b0b09, 0 0 20px -3px rgba(0, 0, 0, 0.5);
text-shadow: 0 0 0 #ffffff, 0 0 5px rgba(0, 0, 0, 0.3);
/* looks */
background-color: #0b0b09;
color: #ffffff;
font-size: 13px;
font-family: sans-serif;
text-decoration: none;
font-weight: bold;
/* ribbon effects */
border: 2px dotted #ffffff;
/* webkit antialias fix */
-webkit-backface-visibility: hidden;
letter-spacing: .5px;
}
</style>
{% include head.html %}
</head>
<body>
<!-- -->
<a href='{{ site.repourl }}' class="ribbon">Fork me</a>
{{ content }}
{% include sidebar.html %}
<!-- Wrap is the content to shift when toggling the sidebar. We wrap the
content to avoid any CSS collisions with our real content. -->
<div class="wrap">
<div class="masthead">
<div class="container">
<h3 class="masthead-title">
<a href="{{ site.baseurl }}/" title="Home">{{ site.title }}</a>
<small>{{ site.tagline }}</small>
</h3>
</div>
</div>
<div class="container content">
{{ content }}
</div>
</div>
<label for="sidebar-checkbox" class="sidebar-toggle"></label>
<script>
(function(document) {
var toggle = document.querySelector('.sidebar-toggle');
var sidebar = document.querySelector('#sidebar');
var checkbox = document.querySelector('#sidebar-checkbox');
document.addEventListener('click', function(e) {
var target = e.target;
if(!checkbox.checked ||
sidebar.contains(target) ||
(target === checkbox || target === toggle)) return;
checkbox.checked = false;
}, false);
})(document);
</script>
</body>
</html>
</html>

8
www/_layouts/page.html Normal file
View File

@ -0,0 +1,8 @@
---
layout: default
---
<div class="page">
<h1 class="page-title">{{ page.title }}</h1>
{{ content }}
</div>

25
www/_layouts/post.html Normal file
View File

@ -0,0 +1,25 @@
---
layout: default
---
<div class="post">
<h1 class="post-title">{{ page.title }}</h1>
<span class="post-date">{{ page.date | date_to_string }}</span>
{{ content }}
</div>
<div class="related">
<h2>Related Posts</h2>
<ul class="related-posts">
{% for post in site.related_posts limit:3 %}
<li>
<h3>
<a href="{{ site.baseurl }}{{ post.url }}">
{{ post.title }}
<small>{{ post.date | date_to_string }}</small>
</a>
</h3>
</li>
{% endfor %}
</ul>
</div>

8
www/_layouts/wiki.html Normal file
View File

@ -0,0 +1,8 @@
---
layout: default
---
<div class="page">
<h1 class="page-title">{{ page.title }}</h1>
{{ content }}
</div>

View File

@ -0,0 +1,14 @@
---
layout: post
title: Welcome to HTTPX the blog
---
First of all, welcome. This is the first post about HTTPX, the ruby http client library for the future.
In it, I'll talk about this library, http, the ruby ecosystem, decisions and choices, and anything that I can relate to the motivation of creating and maintaining httpx. I've realized that there is a lot of resources (blogs, tutorials, etc...) about other ruby (and not only) http client libraries, and practically nothing about httpx, mostly due to it being very recent, and the others having stabilized and being part of mature code bases. Hopefully I can tackle this superficial perception and build some community around it (no matter how good your product is, if no one uses it, it does not exist).
but a milestone has been reached: httpx is now part of the [awesome-ruby resources](https://github.com/markets/awesome-ruby#http-clients-and-tools), so I'd like to celebrate that on our first post.
So long folks. Be excellent to each other.

View File

@ -0,0 +1,176 @@
---
layout: post
title: Falacies about HTTP
---
When I first started working on `httpx`, I wanted to support as many HTTP features and corner-cases as possible. Although I wasn't exhaustively devouring the RFCs looking for things to implement, I was rather hoping that my experience with and knowledge about different http tools (cURL, postman, different http libraries from different languages) could help me narrow them down.
My experience working in software development for product teams also taught me that most software developers aren't aware of these corner cases. In fact, they aren't even aware of the most basic rules regarding the network protocols they use daily, and in fact, many just don't care. When your goal is to get shit done before you go home to your family, these protocols are just a means to an end, and the commoditization of "decent-enough" abstractions around them resulted in the professional devaluation of its thorough knowledge.
Recently, the explosion of packages in software registries for many open source languages/platforms also led to the multiplication of packages which solve the same problem, but just a little bit differently from each other to justify its existence. [awesome-ruby](https://github.com/markets/awesome-ruby#http-clients-and-tools), a self-proclaimed curated list of ruby gems, lists 13 http clients as of the time of writing this article. And this list prefers to omit `net-http`, the http client available in the standard library (the fact that at least 13 alternatives exist for a library shipped in the standard library should already raise some eyebrows).
Some of these packages were probably created by the same developers mentioned above. And the desire to get shit done while ignoring the fundamentals of how the network and its protocols work, led to this state of mostly average implementations who have survived by cheer popularity or "application dependency ossification" (this is a term I just clammed together, meaning "components which use too many resources inefficiently but accomplish the task reasonably, and whose effort to rewrite is offset by the amount of money to keep this elephant running"). This list of falacies is for them.
1. 1 request - 1 response
One of the most spread-out axioms of HTTP is that it is a "request-response" protocol. And in a way, this might have been the way it was designed in the beginning: send a request, receive a response. However, things started getting more complicated.
First, redirects came along. A request would be thrown, a response would come back, but "oh crap!", it has status code 302, 301, the new "Location" is there, so let me send a new request to the new request. It could be quite a few "hops" (see how high level protocols tend to re-use concepts from lower level protocols) until we would get to our resource with a status code 2XX. What is the response in this case?
But this is the simplest bending of "request-response". Then HTTP started being used to upload files. Quite good at it actually, but people started noticing that waiting for the whole request to be sent to then fail on an authentication failure was not a great use of the resources at our disposal. Along came: 100 Continue. In this variant, a Request would send the Headers frame with the "Expect: 100-continue" header, wait on a response from the server, and if this had status code 100, then the Body frame would be sent, and then we would get our final response. So, I count two responses for that interaction. Nevermind that a lot of servers don't implement it (cURL, for instance, sends the body frame if the server doesn't send a response after a few seconds, to circumvent this).
Or take HTTP CONNECT tunnels: In order to send our desired request, we have to first send an HTTP Connect request, receive a successful response (tunnel established) then send our request and get our response.
But one could argue that, for all of the examples above, usually there is a final desired response for a request. So what?
Well, along came HTTP/2 Push. And now, whenever you send a request, you might get N responses, where N - 1 is for potential follow-up requests.
All this to say that, although it looks like a "request-response" protocol, it is actually more complex than that.
Most client implementations choose not to implement these semantics, as they may perceive them as of little value for server-to-server communications, which is where the majority is used.
2. Write the request to socket, Read response from socket
As many of the examples described here, this is a legacy from the early days of TCP/IP, where TCP sockets were always preferred and less complex message interactions were privileged in favour of ease-of-use. As SMTP before it, so did the first versions of HTTP have these semantics built in: open socket, write the full request, receive the full response, close the socket, repeat for next.
However, things started getting complex really fast: HTML pages required multiple resources before being fully rendered. TCP handshake (and later, SSL/TLS) got so much of getting stuff to the end user, that "user hacks" were developed to limit the number of connections. A big chunk of the following revision of HTTP (1.1) revolved around re-using TCP connections (aka "Keep-Alive") and stream data to the end user (aka "chunked encoding"), improvements which were widely adopted by the browsers and improved things for us, browser users. HTTP proxies, the "Host" header, Alt-Svc, TLS SNI, all of them were created to help decrease and manage the number of open/close intermediate links.
Other things were proposed that were good in theory, but hard to deploy in practice. HTTP pipelining was the first attempt at getting multiple responses at once to the end user, but middlebox interference and net gains after request head-of-line blocking meant that this was never going to be a winning strategy, hence there were very few implementations of this feature, and browsers never adopted this widely.
And along came HTTP/2, and TPC-to-HTTP mapping was never the same. Multiple requests and responses multiplexed over the same TCP stream. Push Promises. And maybe the most important, connection coalescing: If you need to contact 2 hosts which share the same IP and share the same TLS certificate, you can now safely pipe them through the same TCP stream!
Many of these improvements have benefitted browsers first and foremost, and things have evolved to minimize the number of network interactions necessary to render an HTML page. HTTP/2 having decreased the number of TCP connections necessary, HTTP/3 will aim at decreasing the number of round-trips necessary. All of this without breaking request and response semantics.
Most of these things aren't as relevant when all you want is send a notification request to a third-party. Therefore, most client implementations choose not to implement most of these semantics. And most are fine implementing "open socket, write request, read response, close socket".
Ruby's `net-http` by default closes the TCP socket after receiving the response (even sending the `Connection: close` header). It does implement keep-alive, but this requires a bit more set-up.
3. Network error is an error, HTTP error is just another response
HTTP status codes can be split into 4 groups:
* 100-199 (informational)
* 200-299 (successful)
* 300-399 (redirection)
* 400-499 (client errors)
* 500-599 (server errors)
In most server-to-server interactions, your code will aim at handling and processing "successful" responses. But in order for this to happen, checking the status code has to happen, in order to ensure that we are getting the expected payload.
In most cases, this check has to be explicit, as 400-599 responses aren't considered an error by clients, and end users have to recover themselves from it.
This is usually not the case for network-level errors. No matter whether the language implements errors as exceptions or return values, this is where network errors will be communicated. A 404 response is a different kind of error, from that perspective. But it is still an error.
This lack of consistency makes code very confusing to read and maintain. 429 and 424 error responses can be retried. 503 responses can be retried. DNS timed-out lookups too. All of these represent operations that can be retried after N seconds. All of them require different error handling schemes, depending of the programming language.
A very interesting solution to handle this can be found in python `requests` library: although network-level errors are bubbled up as exceptions, a 400-599 response can be forced to become an exception by calling `response.raise_for_status`. It's a relative trade-off to reach error consistency, and works well in practice.
However, this becomes a concern when supporting concurrent requests: if you recover from an exception, how do you know which request/response pair caused it? For this case, there's only one answer: prefer return errors than raising errors. Or raise exceptions only after you know whom to address them.
But one thing is clear: Network errors and HTTP errors should be handled at the same level.
4. You send headers, then you send data, then it's done
Earlier, we talked about the "open socket, write request, receive response, close socket" fallacy. But what is "write a request, receive a response" exactly?
HTTP requests and responses are often described as composed of headers and data frames (not to be confused with the HTTP/2 frames). Most of the examples and use cases show header being sent first, then data. This is one of HTTPs basic semantics: no data can be sent before sending headers (again, this might have come from SMTP, if I had to bet).
Things have become a bit more complicated than that. When HTTP started being used for more than just sending "hypertext", other frame sub-sets started showing up.
Along came multipart uploads. Based on MIME multipart messages, which were already being used to transfer non-text data over e-mail (SMTP, again), it created a format for encoding payload-specific information as headers within the HTTP data frame. Here's an example of an 3-file upload request:
```
# from https://stackoverflow.com/questions/913626/what-should-a-multipart-http-request-with-multiple-files-look-like
POST /cgi-bin/qtest HTTP/1.1
Host: aram
User-Agent: Mozilla/5.0 Gecko/2009042316 Firefox/3.0.10
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://aram/~martind/banner.htm
Content-Type: multipart/form-data; boundary=----------287032381131322
Content-Length: 514
------------287032381131322
Content-Disposition: form-data; name="datafile1"; filename="r.gif"
Content-Type: image/gif
GIF87a.............,...........D..;
------------287032381131322
Content-Disposition: form-data; name="datafile2"; filename="g.gif"
Content-Type: image/gif
GIF87a.............,...........D..;
------------287032381131322
Content-Disposition: form-data; name="datafile3"; filename="b.gif"
Content-Type: image/gif
GIF87a.............,...........D..;
------------287032381131322--
```
Later, an addition to HTTP was made: Trailer headers. These are defined as headers which are sent by the peer **after** the data has been transmitted. Its main benefits are beyond the scope of this mention, but this fundamentally changed the expectation of what an HTTP message looks like: after all, headers can be transmitted before and after the data.
A lot of client implementations re-use an already existing HTTP parser. Others write their own. I've seen very few supporting trailer headers. I don't know of any, other than `httpx`, that does (and `httpx` only reliably supports it since ditching `http_parser.rb`, ruby bindings for an outdated version of the node HTTP parser). I also don't know of any in python. Go's `net/http` client supports it.
5. HTTP Bytes are readable
This was particularly talked about during the SPDY days and the initial HTTP/2 draft, when it was decided that the new version was going to adopt binary framing. A lot of different stakeholders voiced their opposition. One of the main arguments was that HTTP plaintext-based framing was a key factor in its adoptions, debuggability and success, and losing this was going make HTTP more dependent of the main companies driving its development (the Googles of this planet).
They were talking about the "telnet my HTTP" days, where due to its text-based nature, it was possible to use the telnet to open a connection to port 80 and just type your request, headers/data, and see the response come in your terminal.
This hasn't been as black-and-white for many years. Due to better resource management, there are time constraints in terms of how much time that "telnet" connection will be kept open by the server (in many cases, if servers don't receive anything within 15 seconds, connection is terminated). HTTPS and encoding negotiation also made telnet-based debugging less efective.
Also, better tooling has showed up that has taken over this problem space: Wireshark has been able to debug HTTP/2 almost since day one, and will be able to debug HTTP/3 in no time.
To sum it up, this fallacy has been a remaining legacy from the old TCP/IP initial protocol days (surprise: you can also send SMTP messages over telnet!). No one should use telnet in 2019 (and I know for a fact that many network providers do). Better tooling has come up for this problem space. Network and system administrators of the 20 years past, just raise the bar.
A hole in a lot of http clients is that they don't provide introspection/debug logging, and one has to resort to network-level tools to inspect payload (`net-http` actually does, however). Maintainers, that should be an easy problem to fix.
6. Response is an IO stream
Some features introduced during the HTTP/1.1 days, like chunked encoding or the event stream API, introduced streaming capabilities to the protocol. This might have given the wrong idea that an HTTP connection was itself streamable, a concept that has "leaked" to a few client implementations.
Usually, in these interactions, You create an HTTP connection (and its inherent TCP/TLS connection), and there is an API that returns the next stream "chunk", after which you can perform some operation, and then loop to the beginning.
Besides the implicit socket-to-HTTP-connection here, which has been debunked a few fallacies ago, there's also the fact that "draining" the connection is only performed when acquiring the next chunk. If your client is not consuming payload as fast as possible, and the server keeps sending, many buffers along the way will be filled waiting for you to consume it. You might just caused "bufferbloat".
If there are timing constraints regarding network operations, there is no guarantee that you'll require the next chunk before the TCP connection itself times out and/or peer aborts. Most of these constraints can be controlled in a dev-only setup, and such interactions will result in "production-only" errors which can't be easily reproduced locally. Surprise, you might just have programmed a "slow client".
This is not to say that you should not react on data frames sent, but usually a callback-based approach is preferred and causes less unexpected behaviour, provided you keep your callbacks small and predictable. But whatever happens, always consume data from the inherent socket as soon as possible.
Besides, if you're using HTTP/2, there is no other chance: unless you can guarantee that there's only one socket for one HTTP/2 connection, you can't just read chunks from it. And even if you can, reading a data chunk involves so much ceremony (flow control, other streams, etc...) that you might as well end up regretting using it in the first place.
Client implementations that map a 1-to-1 relationship between socket and HTTP connection are able to provide such an API, but won't save you from the trouble. If connections hang from the server, time out, or you get blocked from accessing an origin, consider switching.
7. Using HTTP as a transport "dumb pipe"
According to the OSI model, HTTP belongs to layer 7, to the so called application protocols. These are perceived as the higher-level interfaces which programs use to communicate among each other over the network. HTTP is actually a very feature-rich protocol, supporting feature like content-negotiation, caching, virtual hosting, cross-origin resource sharing, tunneling, load balancing, the list goes on.
However, most client use HTTP as a dump pipe where data is sent and received, as if it were a plain TCP stream.
It is like that for many reasons, I'd say. First, there is a big incentive to use HTTP for all the things: bypassing firewalls! Second, implementing all the features of HTTP in a transparent way is rather hard. Some implementers even think that only richer user-agents like browsers would benefit from such features.
Even cURL is partially to blame: it is probably the most widely used and deployed HTTP client around, but its mission is to allow downloading content over many other protocols, where HTTP is just one of them. If you're doing:
```
> curl https://www.google.com
```
You're a) not negotiation payload compression; b) not checking if a cached version of the resource is still up-to-date. Can you do it with cURL? Yes. Do you have to be verbose to do it? Pretty much.
Most 3rd-party JSON API SDKs suffer from this issue, because the underlying library is not doing these things. The only reason why we're sending JSON over HTTP is because proxies have to be bypassed, but it is done in an inefficient way.
## Conclusion
I could had a few more thoughts, but 7 sounds official, so I'll let that sink in.
Enjoy the week, y'all!

View File

@ -20,5 +20,6 @@
}
.badge {
display: inline-block;
margin: 10px 5px;
}

View File

@ -6,14 +6,15 @@ $red: #E74727;
$red-dark: #DC3918;
$gray-light: #f5f5f5;
$white: #fff;
$blue: #268bd2;
// Config
$color-background: $gray-light !default;
$color-dot-accent: $red !default;
$color-dot-accent: $blue !default;
$color-foreground: $black !default;
$color-title: $black !default;
$color-body-text: $black !default;
$color-text-accent: $red-dark !default;
$color-code: $red-dark !default;
$color-nav-link: $red-dark !default;
$color-primary-link: $red-dark !default;
$color-text-accent: $blue !default;
$color-code: $blue !default;
$color-nav-link: $blue !default;
$color-primary-link: $blue !default;

View File

@ -39,11 +39,21 @@
.code-preview {
margin-bottom: 25px;
font-size: 0.7em;
code {
border-radius: 10px;
padding: 10px 50px;
padding: 10px;
font-weight: bold;
background-color: black;
color: white;
}
}
@media screen and (min-width: 1000px) {
.code-preview {
font-size: 1.2em;
code {
padding: 10px 50px;
}
}
}

View File

@ -1,4 +1,5 @@
---
layout: default
---
<header class="header">
@ -20,12 +21,13 @@
HTTPX.get("https://news.ycombinator.com")
</code>
</div>
<a class="btn" href="{{ '/posts/index.html' | prepend: site.baseurl }}">
Blog
</a>
<a class="btn" href="{{ '/rdoc/files/README_md.html' | prepend: site.baseurl }}">
<svg width="16" height="16" viewBox="0 0 1024 1024" fill="currentColor"><path d="M917.806 229.076c-22.212-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.888 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.984 17.78 50.678 41.878 81.374 72.572zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.326 32 32 32h224v624z"></path><path d="M736 832h-448c-17.672 0-32-14.326-32-32s14.328-32 32-32h448c17.674 0 32 14.326 32 32s-14.326 32-32 32z"></path><path d="M736 704h-448c-17.672 0-32-14.326-32-32s14.328-32 32-32h448c17.674 0 32 14.326 32 32s-14.326 32-32 32z"></path><path d="M736 576h-448c-17.672 0-32-14.326-32-32s14.328-32 32-32h448c17.674 0 32 14.326 32 32s-14.326 32-32 32z"></path></svg>
README
</a>
<a class="btn" href="{{ '/wikis/home' | prepend: site.repourl }}">
<svg width="16" height="16" viewBox="0 0 1024 1024" fill="currentColor"><path d="M917.806 229.076c-22.212-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.888 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.984 17.78 50.678 41.878 81.374 72.572zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.326 32 32 32h224v624z"></path><path d="M736 832h-448c-17.672 0-32-14.326-32-32s14.328-32 32-32h448c17.674 0 32 14.326 32 32s-14.326 32-32 32z"></path><path d="M736 704h-448c-17.672 0-32-14.326-32-32s14.328-32 32-32h448c17.674 0 32 14.326 32 32s-14.326 32-32 32z"></path><path d="M736 576h-448c-17.672 0-32-14.326-32-32s14.328-32 32-32h448c17.674 0 32 14.326 32 32s-14.326 32-32 32z"></path></svg>
<a class="btn" href="{{ '/wiki/home.html' | prepend: site.baseurl }}">
WIKI
</a>
</div>
@ -53,7 +55,7 @@
<dl>
{% for plugin in site.data.plugins %}
<dt>
<a href="{{ '/rdoc/classes/HTTPX/Plugins/' | append: plugin.path | prepend: site.baseurl }}">{{ plugin.name }}</a>:
<a href="{{ '/wiki/' | append: plugin.path | prepend: site.baseurl }}">{{ plugin.name }}</a>:
</dt>
<dd>
<div>{{ plugin.description }}</div>
@ -75,6 +77,7 @@
{% endfor %}
</ul>
</section>
</div>
</main>

44
www/posts/index.html Normal file
View File

@ -0,0 +1,44 @@
---
layout: page
title: Blog
pagination:
enabled: true
per_page: 5
collection: "posts"
index_page: "posts.html"
---
<div class="posts">
{% for post in paginator.posts %}
<div class="post">
<h1 class="post-title">
<a href="{{post.url | prepend: site.baseurl}}">
{{ post.title }}
</a>
</h1>
<span class="post-date">{{ post.date | date_to_string }}</span>
{{ post.content }}
</div>
{% endfor %}
</div>
<div class="pagination">
{% if paginator.next_page %}
<a class="pagination-item older" href="{{ site.baseurl }}/page{{paginator.next_page}}">Older</a>
{% else %}
<span class="pagination-item older">Older</span>
{% endif %}
{% if paginator.previous_page %}
{% if paginator.page == 2 %}
<a class="pagination-item newer" href="{{ site.baseurl }}/">Newer</a>
{% else %}
<a class="pagination-item newer" href="{{ site.baseurl }}/page{{paginator.previous_page}}">Newer</a>
{% endif %}
{% else %}
<span class="pagination-item newer">Newer</span>
{% endif %}
</div>

563
www/styles/lanyon.css Normal file
View File

@ -0,0 +1,563 @@
/*
* ___
* /\_ \
* \//\ \ __ ___ __ __ ___ ___
* \ \ \ /'__`\ /' _ `\/\ \/\ \ / __`\ /' _ `\
* \_\ \_/\ \_\.\_/\ \/\ \ \ \_\ \/\ \_\ \/\ \/\ \
* /\____\ \__/.\_\ \_\ \_\/`____ \ \____/\ \_\ \_\
* \/____/\/__/\/_/\/_/\/_/`/___/> \/___/ \/_/\/_/
* /\___/
* \/__/
*
* Designed, built, and released under MIT license by @mdo. Learn more at
* https://github.com/poole/lanyon.
*/
/*
* Contents
*
* Global resets
* Masthead
* Sidebar
* Slide effect
* Posts and pages
* Pagination
* Reverse layout
* Themes
*/
/*
* Global resets
*
* Update the foundational and global aspects of the page.
*/
/* Prevent scroll on narrow devices */
html,
body {
overflow-x: hidden;
}
html {
font-family: "PT Serif", Georgia, "Times New Roman", serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: "PT Sans", Helvetica, Arial, sans-serif;
font-weight: 400;
color: #313131;
letter-spacing: -.025rem;
}
/*
* Wrapper
*
* The wrapper is used to position site content when the sidebar is toggled. We
* use an outter wrap to position the sidebar without interferring with the
* regular page content.
*/
.wrap {
position: relative;
width: 100%;
}
/*
* Container
*
* Center the page content.
*/
.container {
max-width: 28rem;
}
@media (min-width: 38em) {
.container {
max-width: 32rem;
}
}
@media (min-width: 56em) {
.container {
max-width: 38rem;
}
}
/*
* Masthead
*
* Super small header above the content for site name and short description.
*/
.masthead {
padding-top: 1rem;
padding-bottom: 1rem;
margin-bottom: 3rem;
border-bottom: 1px solid #eee;
}
.masthead-title {
margin-top: 0;
margin-bottom: 0;
color: #505050;
}
.masthead-title a {
color: #505050;
}
.masthead-title small {
font-size: 75%;
font-weight: 400;
color: #c0c0c0;
letter-spacing: 0;
}
@media (max-width: 48em) {
.masthead-title {
text-align: center;
}
.masthead-title small {
display: none;
}
}
/*
* Sidebar
*
* The sidebar is the drawer, the item we are toggling with our handy hamburger
* button in the corner of the page.
*
* This particular sidebar implementation was inspired by Chris Coyier's
* "Offcanvas Menu with CSS Target" article, and the checkbox variation from the
* comments by a reader. It modifies both implementations to continue using the
* checkbox (no change in URL means no polluted browser history), but this uses
* `position` for the menu to avoid some potential content reflow issues.
*
* Source: http://css-tricks.com/off-canvas-menu-with-css-target/#comment-207504
*/
/* Style and "hide" the sidebar */
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: -14rem;
width: 14rem;
visibility: hidden;
overflow-y: auto;
font-family: "PT Sans", Helvetica, Arial, sans-serif;
font-size: .875rem; /* 15px */
color: rgba(255,255,255,.6);
background-color: #202020;
-webkit-transition: all .3s ease-in-out;
transition: all .3s ease-in-out;
}
@media (min-width: 30em) {
.sidebar {
font-size: .75rem; /* 14px */
}
}
/* Sidebar content */
.sidebar a {
font-weight: normal;
color: #fff;
}
.sidebar-item {
padding: 1rem;
}
.sidebar-item p:last-child {
margin-bottom: 0;
}
/* Sidebar nav */
.sidebar-nav {
border-bottom: 1px solid rgba(255,255,255,.1);
}
.sidebar-nav-item {
display: block;
padding: .5rem 1rem;
border-top: 1px solid rgba(255,255,255,.1);
}
.sidebar-nav-item.active,
a.sidebar-nav-item:hover,
a.sidebar-nav-item:focus {
text-decoration: none;
background-color: rgba(255,255,255,.1);
border-color: transparent;
}
@media (min-width: 48em) {
.sidebar-item {
padding: 1.5rem;
}
.sidebar-nav-item {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}
/* Hide the sidebar checkbox that we toggle with `.sidebar-toggle` */
.sidebar-checkbox {
position: absolute;
opacity: 0;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
/* Style the `label` that we use to target the `.sidebar-checkbox` */
.sidebar-toggle {
position: absolute;
top: .8rem;
left: 1rem;
display: block;
padding: .25rem .75rem;
color: #505050;
background-color: #fff;
border-radius: .25rem;
cursor: pointer;
}
.sidebar-toggle:before {
display: inline-block;
width: 1rem;
height: .75rem;
content: "";
background-image: -webkit-linear-gradient(to bottom, #555, #555 20%, #fff 20%, #fff 40%, #555 40%, #555 60%, #fff 60%, #fff 80%, #555 80%, #555 100%);
background-image: -moz-linear-gradient(to bottom, #555, #555 20%, #fff 20%, #fff 40%, #555 40%, #555 60%, #fff 60%, #fff 80%, #555 80%, #555 100%);
background-image: -ms-linear-gradient(to bottom, #555, #555 20%, #fff 20%, #fff 40%, #555 40%, #555 60%, #fff 60%, #fff 80%, #555 80%, #555 100%);
background-image: linear-gradient(to bottom, #555, #555 20%, #fff 20%, #fff 40%, #555 40%, #555 60%, #fff 60%, #fff 80%, #555 80%, #555 100%);
}
.sidebar-toggle:active,
#sidebar-checkbox:focus ~ .sidebar-toggle,
#sidebar-checkbox:checked ~ .sidebar-toggle {
color: #fff;
background-color: #555;
}
.sidebar-toggle:active:before,
#sidebar-checkbox:focus ~ .sidebar-toggle:before,
#sidebar-checkbox:checked ~ .sidebar-toggle:before {
background-image: -webkit-linear-gradient(to bottom, #fff, #fff 20%, #555 20%, #555 40%, #fff 40%, #fff 60%, #555 60%, #555 80%, #fff 80%, #fff 100%);
background-image: -moz-linear-gradient(to bottom, #fff, #fff 20%, #555 20%, #555 40%, #fff 40%, #fff 60%, #555 60%, #555 80%, #fff 80%, #fff 100%);
background-image: -ms-linear-gradient(to bottom, #fff, #fff 20%, #555 20%, #555 40%, #fff 40%, #fff 60%, #555 60%, #555 80%, #fff 80%, #fff 100%);
background-image: linear-gradient(to bottom, #fff, #fff 20%, #555 20%, #555 40%, #fff 40%, #fff 60%, #555 60%, #555 80%, #fff 80%, #fff 100%);
}
@media (min-width: 30.1em) {
.sidebar-toggle {
position: fixed;
}
}
@media print {
.sidebar-toggle {
display: none;
}
}
/* Slide effect
*
* Handle the sliding effects of the sidebar and content in one spot, seperate
* from the default styles.
*
* As an a heads up, we don't use `transform: translate3d()` here because when
* mixed with `position: fixed;` for the sidebar toggle, it creates a new
* containing block. Put simply, the fixed sidebar toggle behaves like
* `position: absolute;` when transformed.
*
* Read more about it at http://meyerweb.com/eric/thoughts/2011/09/12/.
*/
.wrap,
.sidebar,
.sidebar-toggle {
-webkit-backface-visibility: hidden;
-ms-backface-visibility: hidden;
backface-visibility: hidden;
}
.wrap,
.sidebar-toggle {
-webkit-transition: -webkit-transform .3s ease-in-out;
transition: transform .3s ease-in-out;
}
#sidebar-checkbox:checked + .sidebar {
z-index: 10;
visibility: visible;
}
#sidebar-checkbox:checked ~ .sidebar,
#sidebar-checkbox:checked ~ .wrap,
#sidebar-checkbox:checked ~ .sidebar-toggle {
-webkit-transform: translateX(14rem);
-ms-transform: translateX(14rem);
transform: translateX(14rem);
}
/*
* Posts and pages
*
* Each post is wrapped in `.post` and is used on default and post layouts. Each
* page is wrapped in `.page` and is only used on the page layout.
*/
.page,
.post {
margin-bottom: 4em;
}
/* Blog post or page title */
.page-title,
.post-title,
.post-title a {
color: #303030;
}
.page-title,
.post-title {
margin-top: 0;
}
/* Meta data line below post title */
.post-date {
display: block;
margin-top: -.5rem;
margin-bottom: 1rem;
color: #9a9a9a;
}
/* Related posts */
.related {
padding-top: 2rem;
padding-bottom: 2rem;
border-top: 1px solid #eee;
}
.related-posts {
padding-left: 0;
list-style: none;
}
.related-posts h3 {
margin-top: 0;
}
.related-posts li small {
font-size: 75%;
color: #999;
}
.related-posts li a:hover {
color: #268bd2;
text-decoration: none;
}
.related-posts li a:hover small {
color: inherit;
}
/*
* Pagination
*
* Super lightweight (HTML-wise) blog pagination. `span`s are provide for when
* there are no more previous or next posts to show.
*/
.pagination {
overflow: hidden; /* clearfix */
margin-left: -1rem;
margin-right: -1rem;
font-family: "PT Sans", Helvetica, Arial, sans-serif;
color: #ccc;
text-align: center;
}
/* Pagination items can be `span`s or `a`s */
.pagination-item {
display: block;
padding: 1rem;
border: 1px solid #eee;
}
.pagination-item:first-child {
margin-bottom: -1px;
}
/* Only provide a hover state for linked pagination items */
a.pagination-item:hover {
background-color: #f5f5f5;
}
@media (min-width: 30em) {
.pagination {
margin: 3rem 0;
}
.pagination-item {
float: left;
width: 50%;
}
.pagination-item:first-child {
margin-bottom: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.pagination-item:last-child {
margin-left: -1px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
/*
* Reverse layout
*
* Flip the orientation of the page by placing the `.sidebar` and sidebar toggle
* on the right side.
*/
.layout-reverse .sidebar {
left: auto;
right: -14rem;
}
.layout-reverse .sidebar-toggle {
left: auto;
right: 1rem;
}
.layout-reverse #sidebar-checkbox:checked ~ .sidebar,
.layout-reverse #sidebar-checkbox:checked ~ .wrap,
.layout-reverse #sidebar-checkbox:checked ~ .sidebar-toggle {
-webkit-transform: translateX(-14rem);
-ms-transform: translateX(-14rem);
transform: translateX(-14rem);
}
/*
* Themes
*
* Apply custom color schemes by adding the appropriate class to the `body`.
* Based on colors from Base16: http://chriskempson.github.io/base16/#default.
*/
/* Red */
.theme-base-08 .sidebar,
.theme-base-08 .sidebar-toggle:active,
.theme-base-08 #sidebar-checkbox:checked ~ .sidebar-toggle {
background-color: #ac4142;
}
.theme-base-08 .container a,
.theme-base-08 .sidebar-toggle,
.theme-base-08 .related-posts li a:hover {
color: #ac4142;
}
/* Orange */
.theme-base-09 .sidebar,
.theme-base-09 .sidebar-toggle:active,
.theme-base-09 #sidebar-checkbox:checked ~ .sidebar-toggle {
background-color: #d28445;
}
.theme-base-09 .container a,
.theme-base-09 .sidebar-toggle,
.theme-base-09 .related-posts li a:hover {
color: #d28445;
}
/* Yellow */
.theme-base-0a .sidebar,
.theme-base-0a .sidebar-toggle:active,
.theme-base-0a #sidebar-checkbox:checked ~ .sidebar-toggle {
background-color: #f4bf75;
}
.theme-base-0a .container a,
.theme-base-0a .sidebar-toggle,
.theme-base-0a .related-posts li a:hover {
color: #f4bf75;
}
/* Green */
.theme-base-0b .sidebar,
.theme-base-0b .sidebar-toggle:active,
.theme-base-0b #sidebar-checkbox:checked ~ .sidebar-toggle {
background-color: #90a959;
}
.theme-base-0b .container a,
.theme-base-0b .sidebar-toggle,
.theme-base-0b .related-posts li a:hover {
color: #90a959;
}
/* Cyan */
.theme-base-0c .sidebar,
.theme-base-0c .sidebar-toggle:active,
.theme-base-0c #sidebar-checkbox:checked ~ .sidebar-toggle {
background-color: #75b5aa;
}
.theme-base-0c .container a,
.theme-base-0c .sidebar-toggle,
.theme-base-0c .related-posts li a:hover {
color: #75b5aa;
}
/* Blue */
.theme-base-0d .sidebar,
.theme-base-0d .sidebar-toggle:active,
.theme-base-0d #sidebar-checkbox:checked ~ .sidebar-toggle {
background-color: #6a9fb5;
}
.theme-base-0d .container a,
.theme-base-0d .sidebar-toggle,
.theme-base-0d .related-posts li a:hover {
color: #6a9fb5;
}
/* Magenta */
.theme-base-0e .sidebar,
.theme-base-0e .sidebar-toggle:active,
.theme-base-0e #sidebar-checkbox:checked ~ .sidebar-toggle {
background-color: #aa759f;
}
.theme-base-0e .container a,
.theme-base-0e .sidebar-toggle,
.theme-base-0e .related-posts li a:hover {
color: #aa759f;
}
/* Brown */
.theme-base-0f .sidebar,
.theme-base-0f .sidebar-toggle:active,
.theme-base-0f #sidebar-checkbox:checked ~ .sidebar-toggle {
background-color: #8f5536;
}
.theme-base-0f .container a,
.theme-base-0f .sidebar-toggle,
.theme-base-0f .related-posts li a:hover {
color: #8f5536;
}
/*
* Overlay sidebar
*
* Make the sidebar content overlay the viewport content instead of pushing it
* aside when toggled.
*/
.sidebar-overlay #sidebar-checkbox:checked ~ .wrap {
-webkit-transform: translateX(0);
-ms-transform: translateX(0);
transform: translateX(0);
}
.sidebar-overlay #sidebar-checkbox:checked ~ .sidebar-toggle {
box-shadow: 0 0 0 .25rem #fff;
}
.sidebar-overlay #sidebar-checkbox:checked ~ .sidebar {
box-shadow: .25rem 0 .5rem rgba(0,0,0,.1);
}
/* Only one tweak for a reverse layout */
.layout-reverse.sidebar-overlay #sidebar-checkbox:checked ~ .sidebar {
box-shadow: -.25rem 0 .5rem rgba(0,0,0,.1);
}

View File

@ -5,16 +5,16 @@
// http://www.colourlovers.com/palette/4335581/Dangerous_Beauty
// #463C3B, #F8DCCE, #FFB063, #EC6933, #DD2F16
@import 'vendor/bourbon/bourbon';
// @import 'vendor/bourbon/bourbon';
@import 'vendor/normalize';
@import 'colors';
@import 'base';
// @import 'base';
@import 'buttons';
@import 'footer';
@import 'grid';
// @import 'footer';
// @import 'grid';
@import 'header';
@import 'utilities';
// @import 'utilities';

430
www/styles/poole.css Normal file
View File

@ -0,0 +1,430 @@
/*
* ___
* /\_ \
* _____ ___ ___\//\ \ __
* /\ '__`\ / __`\ / __`\\ \ \ /'__`\
* \ \ \_\ \/\ \_\ \/\ \_\ \\_\ \_/\ __/
* \ \ ,__/\ \____/\ \____//\____\ \____\
* \ \ \/ \/___/ \/___/ \/____/\/____/
* \ \_\
* \/_/
*
* Designed, built, and released under MIT license by @mdo. Learn more at
* https://github.com/poole/poole.
*/
/*
* Contents
*
* Body resets
* Custom type
* Messages
* Container
* Masthead
* Posts and pages
* Pagination
* Reverse layout
* Themes
*/
/*
* Body resets
*
* Update the foundational and global aspects of the page.
*/
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
html {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.5;
}
@media (min-width: 38em) {
html {
font-size: 20px;
}
}
body {
color: #515151;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
/* No `:visited` state is required by default (browsers will use `a`) */
a {
color: #268bd2;
text-decoration: none;
}
a strong {
color: inherit;
}
/* `:focus` is linked to `:hover` for basic accessibility */
a:hover,
a:focus {
text-decoration: underline;
}
/* Headings */
h1, h2, h3, h4, h5, h6 {
margin-bottom: .5rem;
font-weight: bold;
line-height: 1.25;
color: #313131;
text-rendering: optimizeLegibility;
}
h1 {
font-size: 2rem;
}
h2 {
margin-top: 1rem;
font-size: 1.5rem;
}
h3 {
margin-top: 1.5rem;
font-size: 1.25rem;
}
h4, h5, h6 {
margin-top: 1rem;
font-size: 1rem;
}
/* Body text */
p {
margin-top: 0;
margin-bottom: 1rem;
}
strong {
color: #303030;
}
/* Lists */
ul, ol, dl {
margin-top: 0;
margin-bottom: 1rem;
}
dt {
font-weight: bold;
}
dd {
margin-bottom: .5rem;
}
/* Misc */
hr {
position: relative;
margin: 1.5rem 0;
border: 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #fff;
}
abbr {
font-size: 85%;
font-weight: bold;
color: #555;
text-transform: uppercase;
}
abbr[title] {
cursor: help;
border-bottom: 1px dotted #e5e5e5;
}
/* Code */
code,
pre {
font-family: Menlo, Monaco, "Courier New", monospace;
}
code {
padding: .25em .5em;
font-size: 85%;
color: #bf616a;
background-color: #f9f9f9;
border-radius: 3px;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
padding: 1rem;
font-size: .8rem;
line-height: 1.4;
white-space: pre;
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
background-color: #f9f9f9;
}
pre code {
padding: 0;
font-size: 100%;
color: inherit;
background-color: transparent;
}
/* Pygments via Jekyll */
.highlight {
margin-bottom: 1rem;
border-radius: 4px;
}
.highlight pre {
margin-bottom: 0;
}
/* Gist via GitHub Pages */
.gist .gist-file {
font-family: Menlo, Monaco, "Courier New", monospace !important;
}
.gist .markdown-body {
padding: 15px;
}
.gist pre {
padding: 0;
background-color: transparent;
}
.gist .gist-file .gist-data {
font-size: .8rem !important;
line-height: 1.4;
}
.gist code {
padding: 0;
color: inherit;
background-color: transparent;
border-radius: 0;
}
/* Quotes */
blockquote {
padding: .5rem 1rem;
margin: .8rem 0;
color: #7a7a7a;
border-left: .25rem solid #e5e5e5;
}
blockquote p:last-child {
margin-bottom: 0;
}
@media (min-width: 30em) {
blockquote {
padding-right: 5rem;
padding-left: 1.25rem;
}
}
img {
display: block;
max-width: 100%;
margin: 0 0 1rem;
border-radius: 5px;
}
/* Tables */
table {
margin-bottom: 1rem;
width: 100%;
border: 1px solid #e5e5e5;
border-collapse: collapse;
}
td,
th {
padding: .25rem .5rem;
border: 1px solid #e5e5e5;
}
tbody tr:nth-child(odd) td,
tbody tr:nth-child(odd) th {
background-color: #f9f9f9;
}
/*
* Custom type
*
* Extend paragraphs with `.lead` for larger introductory text.
*/
.lead {
font-size: 1.25rem;
font-weight: 300;
}
/*
* Messages
*
* Show alert messages to users. You may add it to single elements like a `<p>`,
* or to a parent if there are multiple elements to show.
*/
.message {
margin-bottom: 1rem;
padding: 1rem;
color: #717171;
background-color: #f9f9f9;
}
/*
* Container
*
* Center the page content.
*/
.container {
max-width: 38rem;
padding-left: 1rem;
padding-right: 1rem;
margin-left: auto;
margin-right: auto;
}
/*
* Masthead
*
* Super small header above the content for site name and short description.
*/
.masthead {
padding-top: 1rem;
padding-bottom: 1rem;
margin-bottom: 3rem;
}
.masthead-title {
margin-top: 0;
margin-bottom: 0;
color: #505050;
}
.masthead-title a {
color: #505050;
}
.masthead-title small {
font-size: 75%;
font-weight: 400;
color: #c0c0c0;
letter-spacing: 0;
}
/*
* Posts and pages
*
* Each post is wrapped in `.post` and is used on default and post layouts. Each
* page is wrapped in `.page` and is only used on the page layout.
*/
.page,
.post {
margin-bottom: 4em;
}
/* Blog post or page title */
.page-title,
.post-title,
.post-title a {
color: #303030;
}
.page-title,
.post-title {
margin-top: 0;
}
/* Meta data line below post title */
.post-date {
display: block;
margin-top: -.5rem;
margin-bottom: 1rem;
color: #9a9a9a;
}
/* Related posts */
.related {
padding-top: 2rem;
padding-bottom: 2rem;
border-top: 1px solid #eee;
}
.related-posts {
padding-left: 0;
list-style: none;
}
.related-posts h3 {
margin-top: 0;
}
.related-posts li small {
font-size: 75%;
color: #999;
}
.related-posts li a:hover {
color: #268bd2;
text-decoration: none;
}
.related-posts li a:hover small {
color: inherit;
}
/*
* Pagination
*
* Super lightweight (HTML-wise) blog pagination. `span`s are provide for when
* there are no more previous or next posts to show.
*/
.pagination {
overflow: hidden; /* clearfix */
margin-left: -1rem;
margin-right: -1rem;
font-family: "PT Sans", Helvetica, Arial, sans-serif;
color: #ccc;
text-align: center;
}
/* Pagination items can be `span`s or `a`s */
.pagination-item {
display: block;
padding: 1rem;
border: 1px solid #eee;
}
.pagination-item:first-child {
margin-bottom: -1px;
}
/* Only provide a hover state for linked pagination items */
a.pagination-item:hover {
background-color: #f5f5f5;
}
@media (min-width: 30em) {
.pagination {
margin: 3rem 0;
}
.pagination-item {
float: left;
width: 50%;
}
.pagination-item:first-child {
margin-bottom: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.pagination-item:last-child {
margin-left: -1px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}

65
www/styles/syntax.css Normal file
View File

@ -0,0 +1,65 @@
.highlight .hll { background-color: #ffc; }
.highlight .c { color: #999; } /* Comment */
.highlight .err { color: #a00; background-color: #faa } /* Error */
.highlight .k { color: #069; } /* Keyword */
.highlight .o { color: #555 } /* Operator */
.highlight .cm { color: #09f; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #099 } /* Comment.Preproc */
.highlight .c1 { color: #999; } /* Comment.Single */
.highlight .cs { color: #999; } /* Comment.Special */
.highlight .gd { background-color: #fcc; border: 1px solid #c00 } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #f00 } /* Generic.Error */
.highlight .gh { color: #030; } /* Generic.Heading */
.highlight .gi { background-color: #cfc; border: 1px solid #0c0 } /* Generic.Inserted */
.highlight .go { color: #aaa } /* Generic.Output */
.highlight .gp { color: #009; } /* Generic.Prompt */
.highlight .gs { } /* Generic.Strong */
.highlight .gu { color: #030; } /* Generic.Subheading */
.highlight .gt { color: #9c6 } /* Generic.Traceback */
.highlight .kc { color: #069; } /* Keyword.Constant */
.highlight .kd { color: #069; } /* Keyword.Declaration */
.highlight .kn { color: #069; } /* Keyword.Namespace */
.highlight .kp { color: #069 } /* Keyword.Pseudo */
.highlight .kr { color: #069; } /* Keyword.Reserved */
.highlight .kt { color: #078; } /* Keyword.Type */
.highlight .m { color: #f60 } /* Literal.Number */
.highlight .s { color: #d44950 } /* Literal.String */
.highlight .na { color: #4f9fcf } /* Name.Attribute */
.highlight .nb { color: #366 } /* Name.Builtin */
.highlight .nc { color: #0a8; } /* Name.Class */
.highlight .no { color: #360 } /* Name.Constant */
.highlight .nd { color: #99f } /* Name.Decorator */
.highlight .ni { color: #999; } /* Name.Entity */
.highlight .ne { color: #c00; } /* Name.Exception */
.highlight .nf { color: #c0f } /* Name.Function */
.highlight .nl { color: #99f } /* Name.Label */
.highlight .nn { color: #0cf; } /* Name.Namespace */
.highlight .nt { color: #2f6f9f; } /* Name.Tag */
.highlight .nv { color: #033 } /* Name.Variable */
.highlight .ow { color: #000; } /* Operator.Word */
.highlight .w { color: #bbb } /* Text.Whitespace */
.highlight .mf { color: #f60 } /* Literal.Number.Float */
.highlight .mh { color: #f60 } /* Literal.Number.Hex */
.highlight .mi { color: #f60 } /* Literal.Number.Integer */
.highlight .mo { color: #f60 } /* Literal.Number.Oct */
.highlight .sb { color: #c30 } /* Literal.String.Backtick */
.highlight .sc { color: #c30 } /* Literal.String.Char */
.highlight .sd { color: #c30; font-style: italic } /* Literal.String.Doc */
.highlight .s2 { color: #c30 } /* Literal.String.Double */
.highlight .se { color: #c30; } /* Literal.String.Escape */
.highlight .sh { color: #c30 } /* Literal.String.Heredoc */
.highlight .si { color: #a00 } /* Literal.String.Interpol */
.highlight .sx { color: #c30 } /* Literal.String.Other */
.highlight .sr { color: #3aa } /* Literal.String.Regex */
.highlight .s1 { color: #c30 } /* Literal.String.Single */
.highlight .ss { color: #fc3 } /* Literal.String.Symbol */
.highlight .bp { color: #366 } /* Name.Builtin.Pseudo */
.highlight .vc { color: #033 } /* Name.Variable.Class */
.highlight .vg { color: #033 } /* Name.Variable.Global */
.highlight .vi { color: #033 } /* Name.Variable.Instance */
.highlight .il { color: #f60 } /* Literal.Number.Integer.Long */
.css .o,
.css .o + .nt,
.css .nt + .nt { color: #999; }