Agile Web Development with Rails, Edition 4
10.2 Iteration E2: Handling Errors
9.4 Playtime
10.1 Iteration E1: Creating a Smarter Cart
Change the cart to track the quantity of each product.
Add a quantity column to the line_item table in the database.
rails generate migration add_quantity_to_line_items quantity:integer
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from rescue in <class:Exception> at /home/rubys/.rvm/gems/ruby-head-n50123/gems/web-console-2.1.2/lib/web_console/integration/cruby.rb:37)
invoke active_record
create db/migrate/20150330104518_add_quantity_to_line_items.rb
Modify the migration to add a default value for the new column
edit db/migrate/20150330104518_add_quantity_to_line_items.rb
class AddQuantityToLineItems < ActiveRecord::Migration
def change
add_column :line_items, :quantity, :integer, default: 1
end
end
Apply the migration
rake db:migrate
mv 20150330104518_add_quantity_to_line_items.rb 20150330000004_add_quantity_to_line_items.rb
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from rescue in <class:Exception> at /home/rubys/.rvm/gems/ruby-head-n50123/gems/web-console-2.1.2/lib/web_console/integration/cruby.rb:37)
== 20150330000004 AddQuantityToLineItems: migrating ===========================
-- add_column(:line_items, :quantity, :integer, {:default=>1})
-> 0.0035s
== 20150330000004 AddQuantityToLineItems: migrated (0.0036s) ==================
Create a method to add a product to the cart by either incrementing the quantity of an existing line item, or creating a new line item.
edit app/models/cart.rb
def add_product(product_id)
current_item = line_items.find_by(product_id: product_id)
if current_item
current_item.quantity += 1
else
current_item = line_items.build(product_id: product_id)
end
current_item
end
Replace the call to LineItem.new with a call to the new method.
edit app/controllers/line_items_controller.rb
def create
product = Product.find(params[:product_id])
@line_item = @cart.add_product(product.id)
respond_to do |format|
if @line_item.save
format.html { redirect_to @line_item.cart,
notice: 'Line item was successfully created.' }
format.json { render :show,
status: :created, location: @line_item }
else
format.html { render :new }
format.json { render json: @line_item.errors,
status: :unprocessable_entity }
end
end
end
Update the view to show both columns.
edit app/views/carts/show.html.erb
<% if notice %>
<p id="notice"><%= notice %></p>
<% end %>
<h2>Your Pragmatic Cart</h2>
<ul>
<% @cart.line_items.each do |item| %>
<li><%= item.quantity %> × <%= item.product.title %></li>
<% end %>
</ul>
Look at the cart, and see that's not exactly what we intended
get /carts/1
Pragmatic Bookshelf
Your Pragmatic Cart
1 × Programming Ruby 1.9 & 2.0
1 × Programming Ruby 1.9 & 2.0
Generate a migration to combine/separate items in carts.
rails generate migration combine_items_in_cart
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from rescue in <class:Exception> at /home/rubys/.rvm/gems/ruby-head-n50123/gems/web-console-2.1.2/lib/web_console/integration/cruby.rb:37)
invoke active_record
create db/migrate/20150330104521_combine_items_in_cart.rb
Fill in the self.up method
edit db/migrate/20150330104521_combine_items_in_cart.rb
def up
# replace multiple items for a single product in a cart with a single item
Cart.all.each do |cart|
# count the number of each product in the cart
sums = cart.line_items.group(:product_id).sum(:quantity)
sums.each do |product_id, quantity|
if quantity > 1
# remove individual items
cart.line_items.where(product_id: product_id).delete_all
# replace with a single item
item = cart.line_items.build(product_id: product_id)
item.quantity = quantity
item.save!
end
end
end
end
Combine entries
rake db:migrate
mv 20150330104521_combine_items_in_cart.rb 20150330000005_combine_items_in_cart.rb
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from rescue in <class:Exception> at /home/rubys/.rvm/gems/ruby-head-n50123/gems/web-console-2.1.2/lib/web_console/integration/cruby.rb:37)
== 20150330000005 CombineItemsInCart: migrating ===============================
== 20150330000005 CombineItemsInCart: migrated (0.0354s) ======================
Verify that the entries have been combined.
get /carts/1
Pragmatic Bookshelf
Your Pragmatic Cart
2 × Programming Ruby 1.9 & 2.0
Fill in the self.down method
edit db/migrate/20150330000005_combine_items_in_cart.rb
def down
# split items with quantity>1 into multiple items
LineItem.where("quantity>1").each do |line_item|
# add individual items
line_item.quantity.times do
LineItem.create cart_id: line_item.cart_id,
product_id: line_item.product_id, quantity: 1
end
# remove original item
line_item.destroy
end
end
Separate out individual items.
rake db:rollback
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from rescue in <class:Exception> at /home/rubys/.rvm/gems/ruby-head-n50123/gems/web-console-2.1.2/lib/web_console/integration/cruby.rb:37)
== 20150330000005 CombineItemsInCart: reverting ===============================
== 20150330000005 CombineItemsInCart: reverted (0.0320s) ======================
rake db:migrate:status
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from rescue in <class:Exception> at /home/rubys/.rvm/gems/ruby-head-n50123/gems/web-console-2.1.2/lib/web_console/integration/cruby.rb:37)
database: /home/rubys/git/awdwr/edition4/work-220/depot/db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
up 20150330000001 Create products
up 20150330000002 Create carts
up 20150330000003 Create line items
up 20150330000004 Add quantity to line items
down 20150330000005 Combine items in cart
mv db/migrate/20150330000005_combine_items_in_cart.rb db/migrate/20150330000005_combine_items_in_cart.bak
Every item should (once again) only have a quantity of one.
get /carts/1
Pragmatic Bookshelf
Your Pragmatic Cart
1 × Programming Ruby 1.9 & 2.0
1 × Programming Ruby 1.9 & 2.0
Recombine the item data.
mv db/migrate/20150330000005_combine_items_in_cart.bak db/migrate/20150330000005_combine_items_in_cart.rb
rake db:migrate
DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from rescue in <class:Exception> at /home/rubys/.rvm/gems/ruby-head-n50123/gems/web-console-2.1.2/lib/web_console/integration/cruby.rb:37)
== 20150330000005 CombineItemsInCart: migrating ===============================
== 20150330000005 CombineItemsInCart: migrated (0.0340s) ======================
Add a few products to the order.
post /line_items?product_id=2
You are being
redirected .
get http://localhost:3000/carts/1
Pragmatic Bookshelf
Line item was successfully created.
Your Pragmatic Cart
2 × Programming Ruby 1.9 & 2.0
1 × CoffeeScript
post /line_items?product_id=3
You are being
redirected .
get http://localhost:3000/carts/1
Pragmatic Bookshelf
Line item was successfully created.
Your Pragmatic Cart
3 × Programming Ruby 1.9 & 2.0
1 × CoffeeScript
Try something malicious.
get /carts/wibble
ActiveRecord::RecordNotFound
in CartsController#show
Couldn't find Cart with 'id'=wibble
Extracted source (around line #164 ):
162
163
164
165
166
167
record = statement.execute([id], self, connection).first
unless record
raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}"
end
record
rescue RangeError
Extracted source (around line #67 ):
65
66
67
68
69
70
# Use callbacks to share common setup or constraints between actions.
def set_cart
@cart = Cart.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
Extracted source (around line #430 ):
428
429
430
431
432
433
case filter
when Symbol
lambda { |target, _, &blk| target.send filter, &blk }
when String
l = eval "lambda { |value| #{filter} }"
lambda { |target, value| target.instance_exec(value, &l) }
Extracted source (around line #147 ):
145
146
147
148
149
150
if !halted && user_conditions.all? { |c| c.call(target, value) }
result_lambda = -> { user_callback.call target, value }
env.halted = halted_lambda.call(target, result_lambda)
if env.halted
target.send :halted_callback_hook, filter
Extracted source (around line #147 ):
145
146
147
148
149
150
if !halted && user_conditions.all? { |c| c.call(target, value) }
result_lambda = -> { user_callback.call target, value }
env.halted = halted_lambda.call(target, result_lambda)
if env.halted
target.send :halted_callback_hook, filter
Extracted source (around line #12 ):
10
11
12
13
14
15
included do
define_callbacks :process_action,
terminator: ->(controller, result_lambda) { result_lambda.call if result_lambda.is_a?(Proc); controller.response_body },
skip_after_callbacks_if_terminated: true
end
Extracted source (around line #12 ):
10
11
12
13
14
15
included do
define_callbacks :process_action,
terminator: ->(controller, result_lambda) { result_lambda.call if result_lambda.is_a?(Proc); controller.response_body },
skip_after_callbacks_if_terminated: true
end
Extracted source (around line #148 ):
146
147
148
149
150
151
if !halted && user_conditions.all? { |c| c.call(target, value) }
result_lambda = -> { user_callback.call target, value }
env.halted = halted_lambda.call(target, result_lambda)
if env.halted
target.send :halted_callback_hook, filter
end
Extracted source (around line #148 ):
146
147
148
149
150
151
if !halted && user_conditions.all? { |c| c.call(target, value) }
result_lambda = -> { user_callback.call target, value }
env.halted = halted_lambda.call(target, result_lambda)
if env.halted
target.send :halted_callback_hook, filter
end
Extracted source (around line #502 ):
500
501
502
503
504
505
def call(arg)
@before.each { |b| b.call(arg) }
value = @call.call(arg)
@after.each { |a| a.call(arg) }
value
Extracted source (around line #502 ):
500
501
502
503
504
505
def call(arg)
@before.each { |b| b.call(arg) }
value = @call.call(arg)
@after.each { |a| a.call(arg) }
value
Extracted source (around line #502 ):
500
501
502
503
504
505
def call(arg)
@before.each { |b| b.call(arg) }
value = @call.call(arg)
@after.each { |a| a.call(arg) }
value
Extracted source (around line #502 ):
500
501
502
503
504
505
def call(arg)
@before.each { |b| b.call(arg) }
value = @call.call(arg)
@after.each { |a| a.call(arg) }
value
Extracted source (around line #94 ):
92
93
94
95
96
97
runner = callbacks.compile
e = Filters::Environment.new(self, false, nil, block)
runner.call(e).value
end
end
Extracted source (around line #804 ):
802
803
804
805
806
807
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def _run_#{name}_callbacks(&block)
_run_callbacks(_#{name}_callbacks, &block)
end
RUBY
end
Extracted source (around line #83 ):
81
82
83
84
85
86
# end
def run_callbacks(kind, &block)
send "_run_#{kind}_callbacks", &block
end
private
Extracted source (around line #19 ):
17
18
19
20
21
22
# process_action callbacks around the normal behavior.
def process_action(*args)
run_callbacks(:process_action) do
super
end
end
Extracted source (around line #29 ):
27
28
29
30
31
32
private
def process_action(*args)
super
rescue Exception => exception
request.env['action_dispatch.show_detailed_exceptions'] ||= show_detailed_exceptions?
rescue_with_handler(exception) || raise(exception)
Extracted source (around line #32 ):
30
31
32
33
34
35
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
begin
result = super
payload[:status] = response.status
result
ensure
Extracted source (around line #164 ):
162
163
164
165
166
167
def instrument(name, payload = {})
if notifier.listening?(name)
instrumenter.instrument(name, payload) { yield payload if block_given? }
else
yield payload if block_given?
end
Extracted source (around line #20 ):
18
19
20
21
22
23
start name, payload
begin
yield payload
rescue Exception => e
payload[:exception] = [e.class.name, e.message]
raise e
Extracted source (around line #164 ):
162
163
164
165
166
167
def instrument(name, payload = {})
if notifier.listening?(name)
instrumenter.instrument(name, payload) { yield payload if block_given? }
else
yield payload if block_given?
end
Extracted source (around line #30 ):
28
29
30
31
32
33
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
begin
result = super
payload[:status] = response.status
Extracted source (around line #249 ):
247
248
249
250
251
252
request.filtered_parameters.merge! wrapped_filtered_hash
end
super
end
private
Extracted source (around line #18 ):
16
17
18
19
20
21
# and it won't be cleaned up by the method below.
ActiveRecord::LogSubscriber.reset_runtime
super
end
def cleanup_view_runtime
Extracted source (around line #128 ):
126
127
128
129
130
131
@_response_body = nil
process_action(action_name, *args)
end
# Delegates to the class' ::controller_path
Extracted source (around line #30 ):
28
29
30
31
32
33
def process(*) #:nodoc:
old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
super
ensure
I18n.config = old_config
end
Extracted source (around line #194 ):
192
193
194
195
196
197
def dispatch(name, request) #:nodoc:
set_request!(request)
process(name)
to_a
end
Extracted source (around line #241 ):
239
240
241
242
243
244
end
else
lambda { |env| new.dispatch(name, klass.new(env)) }
end
end
end
Extracted source (around line #73 ):
71
72
73
74
75
76
def dispatch(controller, action, env)
controller.action(action).call(env)
end
def normalize_controller!(params)
Extracted source (around line #73 ):
71
72
73
74
75
76
def dispatch(controller, action, env)
controller.action(action).call(env)
end
def normalize_controller!(params)
Extracted source (around line #42 ):
40
41
42
43
44
45
end
dispatch(controller, params[:action], req.env)
end
def prepare_params!(params)
Extracted source (around line #43 ):
41
42
43
44
45
46
req.path_parameters = set_params.merge parameters
status, headers, body = route.app.serve(req)
if 'pass' == headers['X-Cascade']
req.script_name = script_name
Extracted source (around line #30 ):
28
29
30
31
32
33
def serve(req)
find_routes(req).each do |match, parameters, route|
set_params = req.path_parameters
path_info = req.path_info
script_name = req.script_name
Extracted source (around line #30 ):
28
29
30
31
32
33
def serve(req)
find_routes(req).each do |match, parameters, route|
set_params = req.path_parameters
path_info = req.path_info
script_name = req.script_name
Extracted source (around line #769 ):
767
768
769
770
771
772
req = request_class.new(env)
req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
@router.serve(req)
end
def recognize_path(path, environment = {})
Extracted source (around line #24 ):
22
23
24
25
26
27
def call(env)
status, headers, body = @app.call(env)
if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
original_body = body
Extracted source (around line #25 ):
23
24
25
26
27
28
case env[REQUEST_METHOD]
when "GET", "HEAD"
status, headers, body = @app.call(env)
headers = Utils::HeaderHash.new(headers)
if status == 200 && fresh?(env, headers)
status = 304
Extracted source (around line #13 ):
11
12
13
14
15
16
def call(env)
status, headers, body = @app.call(env)
if env[REQUEST_METHOD] == HEAD
[
Extracted source (around line #27 ):
25
26
27
28
29
30
end
@app.call(env)
end
private
Extracted source (around line #266 ):
264
265
266
267
268
269
def call(env)
@app.call(env)
ensure
session = Request::Session.find(env) || {}
flash_hash = env[KEY]
Extracted source (around line #225 ):
223
224
225
226
227
228
def context(env, app=@app)
prepare_session(env)
status, headers, body = app.call(env)
commit_session(env, status, headers, body)
end
Extracted source (around line #220 ):
218
219
220
221
222
223
def call(env)
context(env)
end
def context(env, app=@app)
Extracted source (around line #560 ):
558
559
560
561
562
563
def call(env)
status, headers, body = @app.call(env)
if cookie_jar = env['action_dispatch.cookies']
unless cookie_jar.committed?
Extracted source (around line #36 ):
34
35
36
37
38
39
connection.enable_query_cache!
response = @app.call(env)
response[2] = Rack::BodyProxy.new(response[2]) do
restore_query_cache_settings(connection_id, enabled)
end
Extracted source (around line #642 ):
640
641
642
643
644
645
testing = env['rack.test']
response = @app.call(env)
response[2] = ::Rack::BodyProxy.new(response[2]) do
ActiveRecord::Base.clear_active_connections! unless testing
end
Extracted source (around line #378 ):
376
377
378
379
380
381
end
end
@app.call(env)
end
private
Extracted source (around line #29 ):
27
28
29
30
31
32
result = run_callbacks :call do
begin
@app.call(env)
rescue => error
end
end
Extracted source (around line #90 ):
88
89
90
91
92
93
def _run_callbacks(callbacks, &block)
if callbacks.empty?
block.call if block
else
runner = callbacks.compile
e = Filters::Environment.new(self, false, nil, block)
Extracted source (around line #90 ):
88
89
90
91
92
93
def _run_callbacks(callbacks, &block)
if callbacks.empty?
block.call if block
else
runner = callbacks.compile
e = Filters::Environment.new(self, false, nil, block)
Extracted source (around line #804 ):
802
803
804
805
806
807
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def _run_#{name}_callbacks(&block)
_run_callbacks(_#{name}_callbacks, &block)
end
RUBY
end
Extracted source (around line #83 ):
81
82
83
84
85
86
# end
def run_callbacks(kind, &block)
send "_run_#{kind}_callbacks", &block
end
private
Extracted source (around line #27 ):
25
26
27
28
29
30
def call(env)
error = nil
result = run_callbacks :call do
begin
@app.call(env)
rescue => error
Extracted source (around line #73 ):
71
72
73
74
75
76
prepare!
response = @app.call(env)
response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! }
response
Extracted source (around line #78 ):
76
77
78
79
80
81
def call(env)
env["action_dispatch.remote_ip"] = GetIp.new(env, self)
@app.call(env)
end
# The GetIp class exists as a way to defer processing of the request data
Extracted source (around line #47 ):
45
46
47
48
49
50
def call(env)
_, headers, body = response = @app.call(env)
if headers['X-Cascade'] == 'pass'
body.close if body.respond_to?(:close)
Extracted source (around line #37 ):
35
36
37
38
39
40
end
status, headers, body = @app.call(env)
if exception = env['web_console.exception']
session = Session.from_exception(exception)
Extracted source (around line #30 ):
28
29
30
31
32
33
def call(env)
@app.call(env)
rescue Exception => exception
if env['action_dispatch.show_exceptions'] == false
raise exception
Extracted source (around line #38 ):
36
37
38
39
40
41
instrumenter.start 'request.action_dispatch', request: request
logger.info { started_request_message(request) }
resp = @app.call(env)
resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
resp
rescue Exception
Extracted source (around line #20 ):
18
19
20
21
22
23
if logger.respond_to?(:tagged)
logger.tagged(compute_tags(request)) { call_app(request, env) }
else
call_app(request, env)
end
Extracted source (around line #70 ):
68
69
70
71
72
73
def tagged(*tags)
formatter.tagged(*tags) { yield self }
end
def flush
Extracted source (around line #26 ):
24
25
26
27
28
29
def tagged(*tags)
new_tags = push_tags(*tags)
yield self
ensure
pop_tags(new_tags.size)
end
Extracted source (around line #70 ):
68
69
70
71
72
73
def tagged(*tags)
formatter.tagged(*tags) { yield self }
end
def flush
Extracted source (around line #20 ):
18
19
20
21
22
23
if logger.respond_to?(:tagged)
logger.tagged(compute_tags(request)) { call_app(request, env) }
else
call_app(request, env)
end
Extracted source (around line #25 ):
23
24
25
26
27
28
def call(env)
env[ACTION_DISPATCH_REQUEST_ID] = external_request_id(env) || internal_request_id
@app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = env[ACTION_DISPATCH_REQUEST_ID] }
end
private
Extracted source (around line #22 ):
20
21
22
23
24
25
end
@app.call(env)
end
def method_override(env)
Extracted source (around line #18 ):
16
17
18
19
20
21
def call(env)
start_time = Time.now
status, headers, body = @app.call(env)
request_time = Time.now - start_time
if !headers.has_key?(@header_name)
Extracted source (around line #28 ):
26
27
28
29
30
31
def call(env)
LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
response = @app.call(env)
response[2] = ::Rack::BodyProxy.new(response[2]) do
LocalCacheRegistry.set_cache_for(local_cache_key, nil)
end
Extracted source (around line #17 ):
15
16
17
18
19
20
old, env[FLAG] = env[FLAG], false
@mutex.lock
response = @app.call(env)
body = BodyProxy.new(response[2]) { @mutex.unlock }
response[2] = body
response
Extracted source (around line #115 ):
113
114
115
116
117
118
end
@app.call(env)
end
end
end
Extracted source (around line #113 ):
111
112
113
114
115
116
def call(env)
status, headers, body = @app.call(env)
if body.respond_to?(:to_path)
case type = variation(env)
when 'X-Accel-Redirect'
Extracted source (around line #518 ):
516
517
518
519
520
521
env[routes.env_key] = env['SCRIPT_NAME'].dup
end
app.call(env)
end
# Defines additional Rack env configuration that is added on each call.
Extracted source (around line #165 ):
163
164
165
166
167
168
env["ORIGINAL_FULLPATH"] = req.fullpath
env["ORIGINAL_SCRIPT_NAME"] = req.script_name
super(env)
end
# Reload application routes regardless if they changed or not.
Extracted source (around line #17 ):
15
16
17
18
19
20
old, env[FLAG] = env[FLAG], false
@mutex.lock
response = @app.call(env)
body = BodyProxy.new(response[2]) { @mutex.unlock }
response[2] = body
response
Extracted source (around line #15 ):
13
14
15
16
17
18
def call(env)
status, headers, body = @app.call(env)
headers = HeaderHash.new(headers)
if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
Extracted source (around line #89 ):
87
88
89
90
91
92
env["REQUEST_PATH"] ||= [env["SCRIPT_NAME"], env[PATH_INFO]].join
status, headers, body = @app.call(env)
begin
res.status = status.to_i
headers.each { |k, vs|
Extracted source (around line #138 ):
136
137
138
139
140
141
si = servlet.get_instance(self, *options)
@logger.debug(format("%s is invoked.", si.class.name))
si.service(req, res)
end
##
Extracted source (around line #94 ):
92
93
94
95
96
97
callback.call(req, res)
end
server.service(req, res)
rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout => ex
res.set_error(ex)
rescue HTTPStatus::Error => ex
Extracted source (around line #297 ):
295
296
297
298
299
300
end
call_callback(:AcceptCallback, sock)
block ? block.call(sock) : run(sock)
rescue Errno::ENOTCONN
@logger.debug "Errno::ENOTCONN raised"
rescue ServerError => ex
Rails.root: /home/rubys/git/awdwr/edition4/work-220/depot
Request
Parameters :
{"id"=>"wibble"}
_csrf_token: "YqRzjzaaBuCfr/mg1Zv8lJTMmeNCaXdh7sHwaZ5l9VY="
cart_id: 1
session_id: "a581e31c60d5803defd0c93f5071c210"
GATEWAY_INTERFACE: "CGI/1.1"
HTTP_ACCEPT: "text/html"
HTTP_ACCEPT_ENCODING: "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
REMOTE_ADDR: "127.0.0.1"
REMOTE_HOST: "127.0.0.1"
SERVER_NAME: "localhost"
SERVER_PROTOCOL: "HTTP/1.1"
Response
Headers :
None
10.2 Iteration E2: Handling Errors
9.4 Playtime