diff --git a/spec/std/http/client/response_spec.cr b/spec/std/http/client/response_spec.cr index ea93d588f18b..5d1d2c909657 100644 --- a/spec/std/http/client/response_spec.cr +++ b/spec/std/http/client/response_spec.cr @@ -169,14 +169,14 @@ class HTTP::Client end it "doesn't sets content length for 1xx, 204 or 304" do - [100, 101, 204, 304].each do |status| + [HTTP::Status::CONTINUE, HTTP::Status::SWITCHING_PROTOCOLS, HTTP::Status::NO_CONTENT, HTTP::Status::NOT_MODIFIED].each do |status| response = Response.new(status) response.headers.size.should eq(0) end end it "raises when creating 1xx, 204 or 304 with body" do - [100, 101, 204, 304].each do |status| + [HTTP::Status::CONTINUE, HTTP::Status::SWITCHING_PROTOCOLS, HTTP::Status::NO_CONTENT, HTTP::Status::NOT_MODIFIED].each do |status| expect_raises ArgumentError do Response.new(status, "hello") end @@ -188,7 +188,7 @@ class HTTP::Client headers["Content-Type"] = "text/plain" headers["Content-Length"] = "5" - response = Response.new(200, "hello", headers) + response = Response.new(:ok, "hello", headers) io = IO::Memory.new response.to_io(io) io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhello") @@ -200,7 +200,7 @@ class HTTP::Client headers["Content-Length"] = "5" headers["Set-Cookie"] = "foo=bar; path=/" - response = Response.new(200, "hello", headers) + response = Response.new(:ok, "hello", headers) io = IO::Memory.new response.to_io(io) @@ -221,69 +221,74 @@ class HTTP::Client end it "sets content length from body" do - response = Response.new(200, "hello") + response = Response.new(:ok, "hello") io = IO::Memory.new response.to_io(io) io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello") end it "sets content length even without body" do - response = Response.new(200) + response = Response.new(:ok) io = IO::Memory.new response.to_io(io) io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") end it "serialize as chunked with body_io" do - response = Response.new(200, body_io: IO::Memory.new("hello")) + response = Response.new(:ok, body_io: IO::Memory.new("hello")) io = IO::Memory.new response.to_io(io) io.to_s.should eq("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n") end it "serialize as not chunked with body_io if HTTP/1.0" do - response = Response.new(200, version: "HTTP/1.0", body_io: IO::Memory.new("hello")) + response = Response.new(:ok, version: "HTTP/1.0", body_io: IO::Memory.new("hello")) io = IO::Memory.new response.to_io(io) io.to_s.should eq("HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nhello") end it "returns no content_type when header is missing" do - response = Response.new(200, "") + response = Response.new(:ok, "") response.content_type.should be_nil response.charset.should be_nil end it "returns content type and no charset" do - response = Response.new(200, "", headers: HTTP::Headers{"Content-Type" => "application/octet-stream"}) + response = Response.new(:ok, "", headers: HTTP::Headers{"Content-Type" => "application/octet-stream"}) response.content_type.should eq("application/octet-stream") response.charset.should be_nil end it "returns content type and charset, removes semicolon" do - response = Response.new(200, "", headers: HTTP::Headers{"Content-Type" => "text/plain ; charset=UTF-8"}) + response = Response.new(:ok, "", headers: HTTP::Headers{"Content-Type" => "text/plain ; charset=UTF-8"}) response.content_type.should eq("text/plain") response.charset.should eq("UTF-8") end it "returns content type and charset, removes quotes" do - response = Response.new(200, "", headers: HTTP::Headers{"Content-Type" => %(text/plain ; charset="UTF-8")}) + response = Response.new(:ok, "", headers: HTTP::Headers{"Content-Type" => %(text/plain ; charset="UTF-8")}) response.content_type.should eq("text/plain") response.charset.should eq("UTF-8") end it "returns content type and no charset, other parameter (#2520)" do - response = Response.new(200, "", headers: HTTP::Headers{"Content-Type" => "application/octet-stream ; colenc=U"}) + response = Response.new(:ok, "", headers: HTTP::Headers{"Content-Type" => "application/octet-stream ; colenc=U"}) response.content_type.should eq("application/octet-stream") response.charset.should be_nil end it "returns content type and charset, removes semicolon, with multiple parameters (#2520)" do - response = Response.new(200, "", headers: HTTP::Headers{"Content-Type" => "text/plain ; colenc=U ; charset=UTF-8"}) + response = Response.new(:ok, "", headers: HTTP::Headers{"Content-Type" => "text/plain ; colenc=U ; charset=UTF-8"}) response.content_type.should eq("text/plain") response.charset.should eq("UTF-8") end + it "returns status_code" do + response = Response.new(:created) + response.status_code.should eq 201 + end + it "creates Response with status code 204, no body and Content-Length == 0 (#2512)" do response = Response.new(204, version: "HTTP/1.0", body: "", headers: HTTP::Headers{"Content-Length" => "0"}) response.status_code.should eq(204) @@ -292,7 +297,7 @@ class HTTP::Client describe "success?" do it "returns true for the 2xx" do - response = Response.new(200) + response = Response.new(:ok) response.success?.should eq(true) end diff --git a/spec/std/http/http_spec.cr b/spec/std/http/http_spec.cr index cb0fe186fa41..a490f8e32977 100644 --- a/spec/std/http/http_spec.cr +++ b/spec/std/http/http_spec.cr @@ -77,14 +77,4 @@ describe HTTP do end end end - - describe ".default_status_message_for" do - it "returns a default message for status 200" do - HTTP.default_status_message_for(200).should eq("OK") - end - - it "returns an empty string on non-existent status" do - HTTP.default_status_message_for(0).should eq("") - end - end end diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index 296b2a200343..2a5119d3a530 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -155,10 +155,24 @@ module HTTP io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nHello") end + it "sets status code" do + io = IO::Memory.new + response = Response.new(io) + response.status_code = 201 + response.status.should eq HTTP::Status::CREATED + end + + it "retrieves status code" do + io = IO::Memory.new + response = Response.new(io) + response.status = :created + response.status_code.should eq 201 + end + it "changes status and others" do io = IO::Memory.new response = Response.new(io) - response.status_code = 404 + response.status = :not_found response.version = "HTTP/1.0" response.close io.to_s.should eq("HTTP/1.0 404 Not Found\r\nContent-Length: 0\r\n\r\n") diff --git a/spec/std/http/status_spec.cr b/spec/std/http/status_spec.cr new file mode 100644 index 000000000000..152de22ebfc9 --- /dev/null +++ b/spec/std/http/status_spec.cr @@ -0,0 +1,86 @@ +require "spec" +require "http" + +describe HTTP::Status do + describe ".new" do + it "raises when given invalid status code" do + expect_raises(ArgumentError, "Invalid HTTP status code: 1000") do + HTTP::Status.new(1000) + end + end + + it "returns an instance when given defined status code" do + HTTP::Status.new(201).should eq HTTP::Status::CREATED + end + + it "returns an instance when given undefined status code" do + HTTP::Status.new(418).should eq HTTP::Status.new(418) + end + end + + describe "#code" do + it "returns the status code" do + HTTP::Status::INTERNAL_SERVER_ERROR.code.should eq 500 + end + end + + describe "#informational?" do + it "returns true when given 1xx status code" do + HTTP::Status.new(100).informational?.should be_true + end + + it "returns false unless given 1xx status code" do + HTTP::Status.new(999).informational?.should be_false + end + end + + describe "#success?" do + it "returns true when given 2xx status code" do + HTTP::Status.new(200).success?.should be_true + end + + it "returns false unless given 2xx status code" do + HTTP::Status.new(999).success?.should be_false + end + end + + describe "#redirection?" do + it "returns true when given 3xx status code" do + HTTP::Status.new(300).redirection?.should be_true + end + + it "returns false unless given 3xx status code" do + HTTP::Status.new(999).redirection?.should be_false + end + end + + describe "#client_error?" do + it "returns true when given 4xx status code" do + HTTP::Status.new(400).client_error?.should be_true + end + + it "returns false unless given 4xx status code" do + HTTP::Status.new(999).client_error?.should be_false + end + end + + describe "#server_error?" do + it "returns true when given 5xx status code" do + HTTP::Status.new(500).server_error?.should be_true + end + + it "returns false unless given 5xx status code" do + HTTP::Status.new(999).server_error?.should be_false + end + end + + describe "#description" do + it "returns default description for status 200" do + HTTP::Status.new(200).description.should eq("OK") + end + + it "returns nil on non-existent status" do + HTTP::Status.new(999).description.should eq(nil) + end + end +end diff --git a/src/http/client/response.cr b/src/http/client/response.cr index d92d0351e533..406155c27088 100644 --- a/src/http/client/response.cr +++ b/src/http/client/response.cr @@ -4,24 +4,28 @@ require "mime/media_type" class HTTP::Client::Response getter version : String - getter status_code : Int32 - getter status_message : String + getter status : HTTP::Status + getter status_message : String? getter headers : Headers getter! body_io : IO @cookies : Cookies? - def initialize(@status_code, @body : String? = nil, @headers : Headers = Headers.new, status_message = nil, @version = "HTTP/1.1", @body_io = nil) - @status_message = status_message || HTTP.default_status_message_for(@status_code) + def initialize(@status : HTTP::Status, @body : String? = nil, @headers : Headers = Headers.new, status_message = nil, @version = "HTTP/1.1", @body_io = nil) + @status_message = status_message || @status.description - if Response.mandatory_body?(@status_code) + if Response.mandatory_body?(@status) @body = "" unless @body || @body_io else if (@body || @body_io) && (headers["Content-Length"]? != "0") - raise ArgumentError.new("Status #{status_code} should not have a body") + raise ArgumentError.new("Status #{status.code} should not have a body") end end end + def self.new(status_code : Int32, body : String? = nil, headers : Headers = Headers.new, status_message = nil, version = "HTTP/1.1", body_io = nil) + new(HTTP::Status.new(status_code), body, headers, status_message, version, body_io) + end + def body @body || "" end @@ -32,7 +36,7 @@ class HTTP::Client::Response # Returns `true` if the response status code is between 200 and 299. def success? - (200..299).includes?(status_code) + @status.success? end # Returns a convenience wrapper around querying and setting cookie related @@ -49,6 +53,11 @@ class HTTP::Client::Response mime_type.try &.media_type end + # Convenience method to retrieve the HTTP status code. + def status_code + status.code + end + def charset : String? mime_type.try &.["charset"]? end @@ -60,7 +69,7 @@ class HTTP::Client::Response end def to_io(io) - io << @version << ' ' << @status_code << ' ' << @status_message << "\r\n" + io << @version << ' ' << @status.code << ' ' << @status_message << "\r\n" cookies = @cookies headers = cookies ? cookies.add_response_headers(@headers) : @headers HTTP.serialize_headers_and_body(io, headers, @body, @body_io, @version) @@ -74,8 +83,8 @@ class HTTP::Client::Response end end - def self.mandatory_body?(status_code) : Bool - !(status_code / 100 == 1 || status_code == 204 || status_code == 304) + def self.mandatory_body?(status : HTTP::Status) : Bool + !(status.informational? || status.no_content? || status.not_modified?) end def self.supports_chunked?(version) : Bool @@ -130,14 +139,15 @@ class HTTP::Client::Response raise "Invalid HTTP status code: #{pieces[1]}" end + status = HTTP::Status.new(status_code) status_message = pieces[2]? ? pieces[2].chomp : "" body_type = HTTP::BodyType::OnDemand - body_type = HTTP::BodyType::Mandatory if mandatory_body?(status_code) + body_type = HTTP::BodyType::Mandatory if mandatory_body?(status) body_type = HTTP::BodyType::Prohibited if ignore_body HTTP.parse_headers_and_body(io, body_type: body_type, decompress: decompress) do |headers, body| - return yield new status_code, nil, headers, status_message, http_version, body + return yield new status, nil, headers, status_message, http_version, body end end end diff --git a/src/http/common.cr b/src/http/common.cr index 9e740d5673ce..da6178bd3b13 100644 --- a/src/http/common.cr +++ b/src/http/common.cr @@ -297,87 +297,9 @@ module HTTP quote_string(string, io) end end - - # Returns the default status message of the given HTTP status code. - # - # Based on [Hypertext Transfer Protocol (HTTP) Status Code Registry](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml) - # - # Last Updated 2017-04-14 - # - # HTTP Status Codes (source: [http-status-codes-1.csv](https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv)) - # - # * 1xx: Informational - Request received, continuing process - # * 2xx: Success - The action was successfully received, understood, and accepted - # * 3xx: Redirection - Further action must be taken in order to complete the request - # * 4xx: Client Error - The request contains bad syntax or cannot be fulfilled - # * 5xx: Server Error - The server failed to fulfill an apparently valid request - # - def self.default_status_message_for(status_code : Int) : String - case status_code - when 100 then "Continue" - when 101 then "Switching Protocols" - when 102 then "Processing" - when 200 then "OK" - when 201 then "Created" - when 202 then "Accepted" - when 203 then "Non-Authoritative Information" - when 204 then "No Content" - when 205 then "Reset Content" - when 206 then "Partial Content" - when 207 then "Multi-Status" - when 208 then "Already Reported" - when 226 then "IM Used" - when 300 then "Multiple Choices" - when 301 then "Moved Permanently" - when 302 then "Found" - when 303 then "See Other" - when 304 then "Not Modified" - when 305 then "Use Proxy" - when 307 then "Temporary Redirect" - when 308 then "Permanent Redirect" - when 400 then "Bad Request" - when 401 then "Unauthorized" - when 402 then "Payment Required" - when 403 then "Forbidden" - when 404 then "Not Found" - when 405 then "Method Not Allowed" - when 406 then "Not Acceptable" - when 407 then "Proxy Authentication Required" - when 408 then "Request Timeout" - when 409 then "Conflict" - when 410 then "Gone" - when 411 then "Length Required" - when 412 then "Precondition Failed" - when 413 then "Payload Too Large" - when 414 then "URI Too Long" - when 415 then "Unsupported Media Type" - when 416 then "Range Not Satisfiable" - when 417 then "Expectation Failed" - when 421 then "Misdirected Request" - when 422 then "Unprocessable Entity" - when 423 then "Locked" - when 424 then "Failed Dependency" - when 426 then "Upgrade Required" - when 428 then "Precondition Required" - when 429 then "Too Many Requests" - when 431 then "Request Header Fields Too Large" - when 451 then "Unavailable For Legal Reasons" - when 500 then "Internal Server Error" - when 501 then "Not Implemented" - when 502 then "Bad Gateway" - when 503 then "Service Unavailable" - when 504 then "Gateway Timeout" - when 505 then "HTTP Version Not Supported" - when 506 then "Variant Also Negotiates" - when 507 then "Insufficient Storage" - when 508 then "Loop Detected" - when 510 then "Not Extended" - when 511 then "Network Authentication Required" - else "" - end - end end +require "./status" require "./request" require "./client/response" require "./headers" diff --git a/src/http/formdata.cr b/src/http/formdata.cr index f7d63526f836..a027a56d0912 100644 --- a/src/http/formdata.cr +++ b/src/http/formdata.cr @@ -27,7 +27,7 @@ require "./formdata/**" # end # # unless name && file -# context.response.status_code = 400 +# context.response.status = :bad_request # next # end # diff --git a/src/http/server/handler.cr b/src/http/server/handler.cr index 2bac84ac7a9e..2edbb1307cf0 100644 --- a/src/http/server/handler.cr +++ b/src/http/server/handler.cr @@ -23,7 +23,7 @@ module HTTP::Handler if next_handler = @next next_handler.call(context) else - context.response.status_code = 404 + context.response.status = :not_found context.response.headers["Content-Type"] = "text/plain" context.response.puts "Not Found" end diff --git a/src/http/server/handlers/error_handler.cr b/src/http/server/handlers/error_handler.cr index 09e965f4a3b0..e128db0d9d0f 100644 --- a/src/http/server/handlers/error_handler.cr +++ b/src/http/server/handlers/error_handler.cr @@ -16,7 +16,7 @@ class HTTP::ErrorHandler rescue ex : Exception if @verbose context.response.reset - context.response.status_code = 500 + context.response.status = :internal_server_error context.response.content_type = "text/plain" context.response.print("ERROR: ") ex.inspect_with_backtrace(context.response) diff --git a/src/http/server/handlers/static_file_handler.cr b/src/http/server/handlers/static_file_handler.cr index 5e2d3d78fae4..78e4ebe09daf 100644 --- a/src/http/server/handlers/static_file_handler.cr +++ b/src/http/server/handlers/static_file_handler.cr @@ -29,7 +29,7 @@ class HTTP::StaticFileHandler if @fallthrough call_next(context) else - context.response.status_code = 405 + context.response.status = :method_not_allowed context.response.headers.add("Allow", "GET, HEAD") end return @@ -42,7 +42,7 @@ class HTTP::StaticFileHandler # File path cannot contains '\0' (NUL) because all filesystem I know # don't accept '\0' character as file name. if request_path.includes? '\0' - context.response.status_code = 400 + context.response.status = :bad_request return end @@ -69,7 +69,7 @@ class HTTP::StaticFileHandler add_cache_headers(context.response.headers, last_modified) if cache_request?(context, last_modified) - context.response.status_code = 304 + context.response.status = :not_modified return end @@ -90,7 +90,7 @@ class HTTP::StaticFileHandler end private def redirect_to(context, url) - context.response.status_code = 302 + context.response.status = :found url = URI.escape(url) { |byte| URI.unreserved?(byte) || byte.chr == '/' } context.response.headers.add "Location", url diff --git a/src/http/server/handlers/websocket_handler.cr b/src/http/server/handlers/websocket_handler.cr index 0e25ddd6a516..9d2a85930a90 100644 --- a/src/http/server/handlers/websocket_handler.cr +++ b/src/http/server/handlers/websocket_handler.cr @@ -19,7 +19,7 @@ class HTTP::WebSocketHandler version = context.request.headers["Sec-WebSocket-Version"]? unless version == WebSocket::Protocol::VERSION - response.status_code = 426 + response.status = :upgrade_required response.headers["Sec-WebSocket-Version"] = WebSocket::Protocol::VERSION return end @@ -27,13 +27,13 @@ class HTTP::WebSocketHandler key = context.request.headers["Sec-WebSocket-Key"]? unless key - response.status_code = 400 + response.status = :bad_request return end accept_code = WebSocket::Protocol.key_challenge(key) - response.status_code = 101 + response.status = :switching_protocols response.headers["Upgrade"] = "websocket" response.headers["Connection"] = "Upgrade" response.headers["Sec-WebSocket-Accept"] = accept_code diff --git a/src/http/server/response.cr b/src/http/server/response.cr index 615bec1dcaf5..cd1865e8772d 100644 --- a/src/http/server/response.cr +++ b/src/http/server/response.cr @@ -1,7 +1,7 @@ class HTTP::Server # The response to configure and write to in an `HTTP::Server` handler. # - # The response `status_code` and `headers` must be configured before writing + # The response `status` and `headers` must be configured before writing # the response body. Once response output is written, changing the `status` # and `headers` properties has no effect. # @@ -28,12 +28,12 @@ class HTTP::Server # The status code of this response, which must be set before writing the response # body. If not set, the default value is 200 (OK). - property status_code : Int32 + property status : HTTP::Status # :nodoc: def initialize(@io : IO, @version = "HTTP/1.1") @headers = Headers.new - @status_code = 200 + @status = :ok @wrote_headers = false @upgraded = false @output = output = @original_output = Output.new(@io) @@ -45,7 +45,7 @@ class HTTP::Server # This method is called by RequestProcessor to avoid allocating a new instance for each iteration. @headers.clear @cookies = nil - @status_code = 200 + @status = :ok @wrote_headers = false @upgraded = false @output = @original_output @@ -62,6 +62,16 @@ class HTTP::Server headers["Content-Length"] = content_length.to_s end + # Convenience method to retrieve the HTTP status code. + def status_code + status.code + end + + # Convenience method to set the HTTP status code. + def status_code=(status_code : Int32) + self.status = HTTP::Status.new(status_code) + end + # See `IO#write(slice)`. def write(slice : Bytes) return if slice.empty? @@ -115,15 +125,15 @@ class HTTP::Server # Calls `reset` and then writes the given message. def respond_with_error(message = "Internal Server Error", code = 500) reset - @status_code = code + @status = HTTP::Status.new(code) + message ||= @status.description self.content_type = "text/plain" - self << code << ' ' << message << '\n' + self << @status.code << ' ' << message << '\n' flush end protected def write_headers - status_message = HTTP.default_status_message_for(@status_code) - @io << @version << ' ' << @status_code << ' ' << status_message << "\r\n" + @io << @version << ' ' << @status.code << ' ' << @status.description << "\r\n" headers.each do |name, values| values.each do |value| @io << name << ": " << value << "\r\n" diff --git a/src/http/status.cr b/src/http/status.cr new file mode 100644 index 000000000000..380bffd0cfda --- /dev/null +++ b/src/http/status.cr @@ -0,0 +1,174 @@ +# An enum that provides additional support around HTTP status codes. +# +# Based on [Hypertext Transfer Protocol (HTTP) Status Code Registry](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml) +# +# It provides constants for the defined HTTP status codes as well as helper +# methods to easily identify the type of response. +enum HTTP::Status + CONTINUE = 100 + SWITCHING_PROTOCOLS = 101 + PROCESSING = 102 + EARLY_HINTS = 103 + OK = 200 + CREATED = 201 + ACCEPTED = 202 + NON_AUTHORITATIVE_INFORMATION = 203 + NO_CONTENT = 204 + RESET_CONTENT = 205 + PARTIAL_CONTENT = 206 + MULTI_STATUS = 207 + ALREADY_REPORTED = 208 + IM_USED = 226 + MULTIPLE_CHOICES = 300 + MOVED_PERMANENTLY = 301 + FOUND = 302 + SEE_OTHER = 303 + NOT_MODIFIED = 304 + USE_PROXY = 305 + SWITCH_PROXY = 306 + TEMPORARY_REDIRECT = 307 + PERMANENT_REDIRECT = 308 + BAD_REQUEST = 400 + UNAUTHORIZED = 401 + PAYMENT_REQUIRED = 402 + FORBIDDEN = 403 + NOT_FOUND = 404 + METHOD_NOT_ALLOWED = 405 + NOT_ACCEPTABLE = 406 + PROXY_AUTHENTICATION_REQUIRED = 407 + REQUEST_TIMEOUT = 408 + CONFLICT = 409 + GONE = 410 + LENGTH_REQUIRED = 411 + PRECONDITION_FAILED = 412 + PAYLOAD_TOO_LARGE = 413 + URI_TOO_LONG = 414 + UNSUPPORTED_MEDIA_TYPE = 415 + RANGE_NOT_SATISFIABLE = 416 + EXPECTATION_FAILED = 417 + IM_A_TEAPOT = 418 + MISDIRECTED_REQUEST = 421 + UNPROCESSABLE_ENTITY = 422 + LOCKED = 423 + FAILED_DEPENDENCY = 424 + UPGRADE_REQUIRED = 426 + PRECONDITION_REQUIRED = 428 + TOO_MANY_REQUESTS = 429 + REQUEST_HEADER_FIELDS_TOO_LARGE = 431 + UNAVAILABLE_FOR_LEGAL_REASONS = 451 + INTERNAL_SERVER_ERROR = 500 + NOT_IMPLEMENTED = 501 + BAD_GATEWAY = 502 + SERVICE_UNAVAILABLE = 503 + GATEWAY_TIMEOUT = 504 + HTTP_VERSION_NOT_SUPPORTED = 505 + VARIANT_ALSO_NEGOTIATES = 506 + INSUFFICIENT_STORAGE = 507 + LOOP_DETECTED = 508 + NOT_EXTENDED = 510 + NETWORK_AUTHENTICATION_REQUIRED = 511 + + # Create a new status instance with the given status code, or raise an + # error if the status code given is not inside 100..999. + def self.new(status_code : Int32) + raise ArgumentError.new("Invalid HTTP status code: #{status_code}") unless 100 <= status_code <= 999 + previous_def(status_code) + end + + def code + value + end + + # Returns `true` if the response status code is between 100 and 199. + def informational? + 100 <= code <= 199 + end + + # Returns `true` if the response status code is between 200 and 299. + def success? + 200 <= code <= 299 + end + + # Returns `true` if the response status code is between 300 and 399. + def redirection? + 300 <= code <= 399 + end + + # Returns `true` if the response status code is between 400 and 499. + def client_error? + 400 <= code <= 499 + end + + # Returns `true` if the response status code is between 500 and 599. + def server_error? + 500 <= code <= 599 + end + + # Returns the default status description of the given HTTP status code. + def description : String? + case code + when 100 then "Continue" + when 101 then "Switching Protocols" + when 102 then "Processing" + when 200 then "OK" + when 201 then "Created" + when 202 then "Accepted" + when 203 then "Non-Authoritative Information" + when 204 then "No Content" + when 205 then "Reset Content" + when 206 then "Partial Content" + when 207 then "Multi-Status" + when 208 then "Already Reported" + when 226 then "IM Used" + when 300 then "Multiple Choices" + when 301 then "Moved Permanently" + when 302 then "Found" + when 303 then "See Other" + when 304 then "Not Modified" + when 305 then "Use Proxy" + when 306 then "Switch Proxy" + when 307 then "Temporary Redirect" + when 308 then "Permanent Redirect" + when 400 then "Bad Request" + when 401 then "Unauthorized" + when 402 then "Payment Required" + when 403 then "Forbidden" + when 404 then "Not Found" + when 405 then "Method Not Allowed" + when 406 then "Not Acceptable" + when 407 then "Proxy Authentication Required" + when 408 then "Request Timeout" + when 409 then "Conflict" + when 410 then "Gone" + when 411 then "Length Required" + when 412 then "Precondition Failed" + when 413 then "Payload Too Large" + when 414 then "URI Too Long" + when 415 then "Unsupported Media Type" + when 416 then "Range Not Satisfiable" + when 417 then "Expectation Failed" + when 418 then "I'm a teapot" + when 421 then "Misdirected Request" + when 422 then "Unprocessable Entity" + when 423 then "Locked" + when 424 then "Failed Dependency" + when 426 then "Upgrade Required" + when 428 then "Precondition Required" + when 429 then "Too Many Requests" + when 431 then "Request Header Fields Too Large" + when 451 then "Unavailable For Legal Reasons" + when 500 then "Internal Server Error" + when 501 then "Not Implemented" + when 502 then "Bad Gateway" + when 503 then "Service Unavailable" + when 504 then "Gateway Timeout" + when 505 then "HTTP Version Not Supported" + when 506 then "Variant Also Negotiates" + when 507 then "Insufficient Storage" + when 508 then "Loop Detected" + when 510 then "Not Extended" + when 511 then "Network Authentication Required" + else nil + end + end +end diff --git a/src/http/web_socket/protocol.cr b/src/http/web_socket/protocol.cr index 798bc24f95ec..883d528be57d 100644 --- a/src/http/web_socket/protocol.cr +++ b/src/http/web_socket/protocol.cr @@ -272,8 +272,8 @@ class HTTP::WebSocket::Protocol handshake.to_io(socket) socket.flush handshake_response = HTTP::Client::Response.from_io(socket) - unless handshake_response.status_code == 101 - raise Socket::Error.new("Handshake got denied. Status code was #{handshake_response.status_code}.") + unless handshake_response.status.switching_protocols? + raise Socket::Error.new("Handshake got denied. Status code was #{handshake_response.status.code}.") end challenge_response = Protocol.key_challenge(random_key) diff --git a/src/oauth2/client.cr b/src/oauth2/client.cr index 0380a6eaa088..4e18c65fdf6b 100644 --- a/src/oauth2/client.cr +++ b/src/oauth2/client.cr @@ -145,8 +145,8 @@ class OAuth2::Client } response = HTTP::Client.post(token_uri, form: body, headers: headers) - case response.status_code - when 200, 201 + case response.status + when .ok?, .created? OAuth2::AccessToken.from_json(response.body) else raise OAuth2::Error.from_json(response.body)