Skip to content

Commit

Permalink
Make o.p.client.http.Status safe against unknown HTTP status codes (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
snazy authored Jul 10, 2024
1 parent f78f6f1 commit 78e81f7
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 53 deletions.
197 changes: 150 additions & 47 deletions api/client/src/main/java/org/projectnessie/client/http/Status.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,55 +15,132 @@
*/
package org.projectnessie.client.http;

import static org.projectnessie.client.http.impl.HttpUtils.checkArgument;

import java.util.Arrays;

/** HTTP request status enum. Map return code to concrete status type with message. */
public enum Status {
OK(200, "OK"),
CREATED(201, "Created"),
ACCEPTED(202, "Accepted"),
NO_CONTENT(204, "No Content"),
RESET_CONTENT(205, "Reset Content"),
PARTIAL_CONTENT(206, "Partial Content"),
MOVED_PERMANENTLY(301, "Moved Permanently"),
FOUND(302, "Found"),
SEE_OTHER(303, "See Other"),
NOT_MODIFIED(304, "Not Modified"),
USE_PROXY(305, "Use Proxy"),
TEMPORARY_REDIRECT(307, "Temporary Redirect"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
PAYMENT_REQUIRED(402, "Payment Required"),
FORBIDDEN(403, "Forbidden"),
NOT_FOUND(404, "Not Found"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
NOT_ACCEPTABLE(406, "Not Acceptable"),
PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"),
REQUEST_TIMEOUT(408, "Request Timeout"),
CONFLICT(409, "Conflict"),
GONE(410, "Gone"),
LENGTH_REQUIRED(411, "Length Required"),
PRECONDITION_FAILED(412, "Precondition Failed"),
REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large"),
REQUEST_URI_TOO_LONG(414, "Request-URI Too Long"),
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
EXPECTATION_FAILED(417, "Expectation Failed"),
PRECONDITION_REQUIRED(428, "Precondition Required"),
TOO_MANY_REQUESTS(429, "Too Many Requests"),
REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
NOT_IMPLEMENTED(501, "Not Implemented"),
BAD_GATEWAY(502, "Bad Gateway"),
SERVICE_UNAVAILABLE(503, "Service Unavailable"),
GATEWAY_TIMEOUT(504, "Gateway Timeout"),
HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version Not Supported"),
NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required");
public final class Status {

private static final Status[][] LINES = new Status[10][];

static {
Arrays.fill(LINES, new Status[0]);
}

public static final int OK_CODE = 200;
public static final Status OK = addCode(OK_CODE, "OK");
public static final int CREATED_CODE = 201;
public static final Status CREATED = addCode(CREATED_CODE, "Created");
public static final int ACCEPTED_CODE = 202;
public static final Status ACCEPTED = addCode(ACCEPTED_CODE, "Accepted");
public static final int NO_CONTENT_CODE = 204;
public static final Status NO_CONTENT = addCode(NO_CONTENT_CODE, "No Content");
public static final int RESET_CONTENT_CODE = 205;
public static final Status RESET_CONTENT = addCode(RESET_CONTENT_CODE, "Reset Content");
public static final int PARTIAL_CONTENT_CODE = 206;
public static final Status PARTIAL_CONTENT = addCode(PARTIAL_CONTENT_CODE, "Partial Content");
public static final int MOVED_PERMANENTLY_CODE = 301;
public static final Status MOVED_PERMANENTLY =
addCode(MOVED_PERMANENTLY_CODE, "Moved Permanently");
public static final int FOUND_CODE = 302;
public static final Status FOUND = addCode(FOUND_CODE, "Found");
public static final int SEE_OTHER_CODE = 303;
public static final Status SEE_OTHER = addCode(SEE_OTHER_CODE, "See Other");
public static final int NOT_MODIFIED_CODE = 304;
public static final Status NOT_MODIFIED = addCode(NOT_MODIFIED_CODE, "Not Modified");
public static final int USE_PROXY_CODE = 305;
public static final Status USE_PROXY = addCode(USE_PROXY_CODE, "Use Proxy");
public static final int TEMPORARY_REDIRECT_CODE = 307;
public static final Status TEMPORARY_REDIRECT =
addCode(TEMPORARY_REDIRECT_CODE, "Temporary Redirect");
public static final int BAD_REQUEST_CODE = 400;
public static final Status BAD_REQUEST = addCode(BAD_REQUEST_CODE, "Bad Request");
public static final int UNAUTHORIZED_CODE = 401;
public static final Status UNAUTHORIZED = addCode(UNAUTHORIZED_CODE, "Unauthorized");
public static final int PAYMENT_REQUIRED_CODE = 402;
public static final Status PAYMENT_REQUIRED = addCode(PAYMENT_REQUIRED_CODE, "Payment Required");
public static final int FORBIDDEN_CODE = 403;
public static final Status FORBIDDEN = addCode(FORBIDDEN_CODE, "Forbidden");
public static final int NOT_FOUND_CODE = 404;
public static final Status NOT_FOUND = addCode(NOT_FOUND_CODE, "Not Found");
public static final int METHOD_NOT_ALLOWED_CODE = 405;
public static final Status METHOD_NOT_ALLOWED =
addCode(METHOD_NOT_ALLOWED_CODE, "Method Not Allowed");
public static final int NOT_ACCEPTABLE_CODE = 406;
public static final Status NOT_ACCEPTABLE = addCode(NOT_ACCEPTABLE_CODE, "Not Acceptable");
public static final int PROXY_AUTHENTICATION_REQUIRED_CODE = 407;
public static final Status PROXY_AUTHENTICATION_REQUIRED =
addCode(PROXY_AUTHENTICATION_REQUIRED_CODE, "Proxy Authentication Required");
public static final int REQUEST_TIMEOUT_CODE = 408;
public static final Status REQUEST_TIMEOUT = addCode(REQUEST_TIMEOUT_CODE, "Request Timeout");
public static final int CONFLICT_CODE = 409;
public static final Status CONFLICT = addCode(CONFLICT_CODE, "Conflict");
public static final int GONE_CODE = 410;
public static final Status GONE = addCode(GONE_CODE, "Gone");
public static final int LENGTH_REQUIRED_CODE = 411;
public static final Status LENGTH_REQUIRED = addCode(LENGTH_REQUIRED_CODE, "Length Required");
public static final int PRECONDITION_FAILED_CODE = 412;
public static final Status PRECONDITION_FAILED =
addCode(PRECONDITION_FAILED_CODE, "Precondition Failed");
public static final int REQUEST_ENTITY_TOO_LARGE_CODE = 413;
public static final Status REQUEST_ENTITY_TOO_LARGE =
addCode(REQUEST_ENTITY_TOO_LARGE_CODE, "Request Entity Too Large");
public static final int REQUEST_URI_TOO_LONG_CODE = 414;
public static final Status REQUEST_URI_TOO_LONG =
addCode(REQUEST_URI_TOO_LONG_CODE, "Request-URI Too Long");
public static final int UNSUPPORTED_MEDIA_TYPE_CODE = 415;
public static final Status UNSUPPORTED_MEDIA_TYPE =
addCode(UNSUPPORTED_MEDIA_TYPE_CODE, "Unsupported Media Type");
public static final int REQUESTED_RANGE_NOT_SATISFIABLE_CODE = 416;
public static final Status REQUESTED_RANGE_NOT_SATISFIABLE =
addCode(REQUESTED_RANGE_NOT_SATISFIABLE_CODE, "Requested Range Not Satisfiable");
public static final int EXPECTATION_FAILED_CODE = 417;
public static final Status EXPECTATION_FAILED =
addCode(EXPECTATION_FAILED_CODE, "Expectation Failed");
public static final int PRECONDITION_REQUIRED_CODE = 428;
public static final Status PRECONDITION_REQUIRED =
addCode(PRECONDITION_REQUIRED_CODE, "Precondition Required");
public static final int TOO_MANY_REQUESTS_CODE = 429;
public static final Status TOO_MANY_REQUESTS =
addCode(TOO_MANY_REQUESTS_CODE, "Too Many Requests");
public static final int REQUEST_HEADER_FIELDS_TOO_LARGE_CODE = 431;
public static final Status REQUEST_HEADER_FIELDS_TOO_LARGE =
addCode(REQUEST_HEADER_FIELDS_TOO_LARGE_CODE, "Request Header Fields Too Large");
public static final int INTERNAL_SERVER_ERROR_CODE = 500;
public static final Status INTERNAL_SERVER_ERROR =
addCode(INTERNAL_SERVER_ERROR_CODE, "Internal Server Error");
public static final int NOT_IMPLEMENTED_CODE = 501;
public static final Status NOT_IMPLEMENTED = addCode(NOT_IMPLEMENTED_CODE, "Not Implemented");
public static final int BAD_GATEWAY_CODE = 502;
public static final Status BAD_GATEWAY = addCode(BAD_GATEWAY_CODE, "Bad Gateway");
public static final int SERVICE_UNAVAILABLE_CODE = 503;
public static final Status SERVICE_UNAVAILABLE =
addCode(SERVICE_UNAVAILABLE_CODE, "Service Unavailable");
public static final int GATEWAY_TIMEOUT_CODE = 504;
public static final Status GATEWAY_TIMEOUT = addCode(GATEWAY_TIMEOUT_CODE, "Gateway Timeout");
public static final int HTTP_VERSION_NOT_SUPPORTED_CODE = 505;
public static final Status HTTP_VERSION_NOT_SUPPORTED =
addCode(HTTP_VERSION_NOT_SUPPORTED_CODE, "HTTP Version Not Supported");
public static final int NETWORK_AUTHENTICATION_REQUIRED_CODE = 511;
public static final Status NETWORK_AUTHENTICATION_REQUIRED =
addCode(NETWORK_AUTHENTICATION_REQUIRED_CODE, "Network Authentication Required");

private final int code;
private final String reason;

Status(final int statusCode, final String reason) {
private static Status addCode(int code, String reason) {
Status[][] lines = LINES;
int line = code / 100;
int code100 = code % 100;
Status[] l = lines[line];
if (l.length < code100 + 1) {
l = lines[line] = Arrays.copyOf(l, code100 + 1);
}
return l[code100] = new Status(code, reason);
}

private Status(int statusCode, String reason) {
this.code = statusCode;
this.reason = reason;
}
Expand All @@ -76,11 +153,16 @@ public enum Status {
* @throws UnsupportedOperationException if unknown status code
*/
public static Status fromCode(int code) {
return Arrays.stream(Status.values())
.filter(x -> x.code == code)
.findFirst()
.orElseThrow(
() -> new UnsupportedOperationException(String.format("Unknown status code %d", code)));
checkArgument(code >= 0 && code < 1000, "Illegal HTTP status code %d", code);
int code100 = code % 100;
Status[] l = LINES[code / 100];
if (l.length > code100) {
Status s = l[code100];
if (s != null) {
return s;
}
}
return new Status(code, "");
}

public int getCode() {
Expand All @@ -90,4 +172,25 @@ public int getCode() {
public String getReason() {
return reason;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Status status = (Status) o;
return code == status.code && reason.equals(status.reason);
}

@Override
public int hashCode() {
int result = code;
result = 31 * result + reason.hashCode();
return result;
}

@Override
public String toString() {
return code + "/" + reason;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/
package org.projectnessie.client.rest;

import static org.projectnessie.client.http.Status.INTERNAL_SERVER_ERROR_CODE;
import static org.projectnessie.client.http.Status.SERVICE_UNAVAILABLE_CODE;
import static org.projectnessie.client.http.Status.UNAUTHORIZED_CODE;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
Expand Down Expand Up @@ -65,14 +69,14 @@ public static void checkResponse(ResponseContext con) throws Exception {
// exception with some level of break-down by sub-class to allow for intelligent exception
// handling on the caller side.
Exception exception;
switch (status) {
case INTERNAL_SERVER_ERROR:
switch (status.getCode()) {
case INTERNAL_SERVER_ERROR_CODE:
exception = new NessieInternalServerException(error);
break;
case SERVICE_UNAVAILABLE:
case SERVICE_UNAVAILABLE_CODE:
exception = new NessieUnavailableException(error);
break;
case UNAUTHORIZED:
case UNAUTHORIZED_CODE:
// Note: UNAUTHORIZED at this point cannot be a Nessie-controlled error.
// It must be an error reported at a higher level HTTP service in front of the Nessie
// Server.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.projectnessie.client.http.impl.HttpRuntimeConfig;
import org.projectnessie.client.util.HttpTestServer;
Expand Down Expand Up @@ -471,8 +470,17 @@ void testGetTemplate() throws Exception {
}
}

static Stream<Status> testHttpResponses() {
IntStream l2 = IntStream.rangeClosed(200, 206);
IntStream l3 = IntStream.rangeClosed(300, 308);
IntStream l4 = IntStream.rangeClosed(400, 440);
IntStream l5 = IntStream.rangeClosed(500, 510);
return IntStream.concat(l2, IntStream.concat(l3, IntStream.concat(l4, l5)))
.mapToObj(Status::fromCode);
}

@ParameterizedTest
@EnumSource(Status.class)
@MethodSource
void testHttpResponses(Status status) throws Exception {
// HTTP/304 defines that no response body must be sent
assumeThat(status).isNotEqualTo(Status.NOT_MODIFIED);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2024 Dremio
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.projectnessie.client.http;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;

import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

public class TestStatus {
@ParameterizedTest
@MethodSource
public void statusCodes(int code) {
assertThat(Status.fromCode(code)).isNotNull().extracting(Status::getCode).isEqualTo(code);
assertThat(Status.fromCode(code)).isNotNull().extracting(Status::getReason).isNotNull();
}

static Stream<Integer> statusCodes() {
return IntStream.rangeClosed(0, 999).boxed();
}

@ParameterizedTest
@MethodSource
public void invalidStatusCodes(int code) {
assertThatIllegalArgumentException()
.isThrownBy(() -> Status.fromCode(code))
.withMessage("Illegal HTTP status code %d", code);
}

static Stream<Integer> invalidStatusCodes() {
return IntStream.of(-1, -100, 1000, Integer.MAX_VALUE, Integer.MIN_VALUE).boxed();
}
}

0 comments on commit 78e81f7

Please sign in to comment.