
com.linecorp.armeria.internal.ArmeriaHttpUtil Maven / Gradle / Ivy
/*
* Copyright 2016 LINE Corporation
*
* LINE Corporation licenses this file to you 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:
*
* https://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.
*/
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you 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:
*
* https://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 com.linecorp.armeria.internal;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static io.netty.handler.codec.http.HttpUtil.isAsteriskForm;
import static io.netty.handler.codec.http.HttpUtil.isOriginForm;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.streamError;
import static io.netty.util.AsciiString.EMPTY_STRING;
import static io.netty.util.ByteProcessor.FIND_COMMA;
import static io.netty.util.internal.StringUtil.decodeHexNibble;
import static io.netty.util.internal.StringUtil.isNullOrEmpty;
import static io.netty.util.internal.StringUtil.length;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringJoiner;
import javax.annotation.Nullable;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.linecorp.armeria.common.DefaultHttpHeaders;
import com.linecorp.armeria.common.Flags;
import com.linecorp.armeria.common.HttpData;
import com.linecorp.armeria.common.HttpHeaderNames;
import com.linecorp.armeria.common.HttpHeaders;
import com.linecorp.armeria.common.HttpMethod;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.HttpStatusClass;
import io.netty.handler.codec.DefaultHeaders;
import io.netty.handler.codec.UnsupportedValueConverter;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.DefaultHttp2Headers;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames;
import io.netty.util.AsciiString;
import io.netty.util.HashingStrategy;
import io.netty.util.internal.StringUtil;
/**
* Provides various utility functions for internal use related with HTTP.
*
* The conversion between HTTP/1 and HTTP/2 has been forked from Netty's {@link HttpConversionUtil}.
*/
public final class ArmeriaHttpUtil {
// Forked from Netty at 7d213240ca768d6dd35ef2336b1fda757bd4df3c
/**
* The default case-insensitive {@link AsciiString} hasher and comparator for HTTP/2 headers.
*/
public static final HashingStrategy HTTP2_HEADER_NAME_HASHER =
new HashingStrategy() {
@Override
public int hashCode(AsciiString o) {
return o.hashCode();
}
@Override
public boolean equals(AsciiString a, AsciiString b) {
return a.contentEqualsIgnoreCase(b);
}
};
private static final URI ROOT = URI.create("/");
/**
* The set of headers that should not be directly copied when converting headers from HTTP/1 to HTTP/2.
*/
private static final CharSequenceMap HTTP_TO_HTTP2_HEADER_BLACKLIST = new CharSequenceMap();
/**
* The set of headers that should not be directly copied when converting headers from HTTP/2 to HTTP/1.
*/
private static final CharSequenceMap HTTP2_TO_HTTP_HEADER_BLACKLIST = new CharSequenceMap();
static {
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.CONNECTION, EMPTY_STRING);
@SuppressWarnings("deprecation")
final AsciiString keepAlive = HttpHeaderNames.KEEP_ALIVE;
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(keepAlive, EMPTY_STRING);
@SuppressWarnings("deprecation")
final AsciiString proxyConnection = HttpHeaderNames.PROXY_CONNECTION;
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(proxyConnection, EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.HOST, EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.UPGRADE, EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING);
HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.PATH.text(), EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.AUTHORITY, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.METHOD, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.PATH, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.SCHEME, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.STATUS, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(HttpHeaderNames.TRAILER, EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING);
HTTP2_TO_HTTP_HEADER_BLACKLIST.add(ExtensionHeaderNames.PATH.text(), EMPTY_STRING);
}
/**
* Translations from HTTP/2 header name to the HTTP/1.x equivalent.
*/
private static final CharSequenceMap REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap();
private static final CharSequenceMap RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap();
static {
RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
HttpHeaderNames.HOST);
REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS);
}
/**
* rfc7540, 8.1.2.3 states the path must not
* be empty, and instead should be {@code /}.
*/
private static final String EMPTY_REQUEST_PATH = "/";
private static final Splitter COOKIE_SPLITTER = Splitter.on(';').trimResults().omitEmptyStrings();
private static final String COOKIE_SEPARATOR = "; ";
@Nullable
private static final LoadingCache HEADER_VALUE_CACHE =
Flags.headerValueCacheSpec().map(ArmeriaHttpUtil::buildCache).orElse(null);
private static final Set CACHED_HEADERS = Flags.cachedHeaders().stream().map(AsciiString::of)
.collect(toImmutableSet());
private static LoadingCache buildCache(String spec) {
return Caffeine.from(spec).build(AsciiString::toString);
}
/**
* Concatenates two path strings.
*/
public static String concatPaths(@Nullable String path1, @Nullable String path2) {
path2 = path2 == null ? "" : path2;
if (path1 == null || path1.isEmpty() || EMPTY_REQUEST_PATH.equals(path1)) {
if (path2.isEmpty()) {
return EMPTY_REQUEST_PATH;
}
if (path2.charAt(0) == '/') {
return path2; // Most requests will land here.
}
return new StringBuilder(path2.length() + 1)
.append('/').append(path2).toString();
}
// At this point, we are sure path1 is neither empty nor null.
if (path2.isEmpty()) {
// Only path1 is non-empty. No need to concatenate.
return path1;
}
if (path1.charAt(path1.length() - 1) == '/') {
if (path2.charAt(0) == '/') {
// path1 ends with '/' and path2 starts with '/'.
// Avoid double-slash by stripping the first slash of path2.
return new StringBuilder(path1.length() + path2.length() - 1)
.append(path1).append(path2, 1, path2.length()).toString();
}
// path1 ends with '/' and path2 does not start with '/'.
// Simple concatenation would suffice.
return new StringBuilder(path1.length() + path2.length())
.append(path1).append(path2).toString();
}
if (path2.charAt(0) == '/') {
// path1 does not end with '/' and path2 starts with '/'.
// Simple concatenation would suffice.
return path1 + path2;
}
// path1 does not end with '/' and path2 does not start with '/'.
// Need to insert '/' between path1 and path2.
return new StringBuilder(path1.length() + path2.length() + 1)
.append(path1).append('/').append(path2).toString();
}
/**
* Decodes a percent-encoded path string.
*/
public static String decodePath(String path) {
if (path.indexOf('%') < 0) {
// No need to decoded; not percent-encoded
return path;
}
// Decode percent-encoded characters.
// An invalid character is replaced with 0xFF, which will be replaced into '�' by UTF-8 decoder.
final int len = path.length();
final byte[] buf = new byte[len];
int dstLen = 0;
for (int i = 0; i < len; i++) {
final char ch = path.charAt(i);
if (ch != '%') {
buf[dstLen++] = (byte) ((ch & 0xFF80) == 0 ? ch : 0xFF);
continue;
}
// Decode a percent-encoded character.
final int hexEnd = i + 3;
if (hexEnd > len) {
// '%' or '%x' (must be followed by two hexadigits)
buf[dstLen++] = (byte) 0xFF;
break;
}
final int digit1 = decodeHexNibble(path.charAt(++i));
final int digit2 = decodeHexNibble(path.charAt(++i));
if (digit1 < 0 || digit2 < 0) {
// The first or second digit is not hexadecimal.
buf[dstLen++] = (byte) 0xFF;
} else {
buf[dstLen++] = (byte) ((digit1 << 4) | digit2);
}
}
return new String(buf, 0, dstLen, StandardCharsets.UTF_8);
}
/**
* Returns {@code true} if the content of the response with the given {@link HttpStatus} is expected to
* be always empty (1xx, 204, 205 and 304 responses.)
*/
public static boolean isContentAlwaysEmpty(HttpStatus status) {
if (status.codeClass() == HttpStatusClass.INFORMATIONAL) {
return true;
}
switch (status.code()) {
case 204: case 205: case 304:
return true;
}
return false;
}
/**
* Returns {@code true} if the content of the response with the given {@link HttpStatus} is expected to
* be always empty (1xx, 204, 205 and 304 responses.)
*
* @throws IllegalArgumentException if the specified {@code content} or {@code trailingHeaders} are
* non-empty when the content is always empty
*/
public static boolean isContentAlwaysEmptyWithValidation(
HttpStatus status, HttpData content, HttpHeaders trailingHeaders) {
if (!isContentAlwaysEmpty(status)) {
return false;
}
if (!content.isEmpty()) {
throw new IllegalArgumentException(
"A " + status + " response must have empty content: " + content.length() + " byte(s)");
}
if (!trailingHeaders.isEmpty()) {
throw new IllegalArgumentException(
"A " + status + " response must not have trailing headers: " + trailingHeaders);
}
return true;
}
/**
* Converts the specified Netty HTTP/2 into Armeria HTTP/2 headers.
*/
public static HttpHeaders toArmeria(Http2Headers headers, boolean endOfStream) {
final HttpHeaders converted = new DefaultHttpHeaders(false, headers.size(), endOfStream);
StringJoiner cookieJoiner = null;
for (Entry e : headers) {
final AsciiString name = AsciiString.of(e.getKey());
final CharSequence value = e.getValue();
// Cookies must be concatenated into a single octet string.
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5
if (name.equals(HttpHeaderNames.COOKIE)) {
if (cookieJoiner == null) {
cookieJoiner = new StringJoiner(COOKIE_SEPARATOR);
}
COOKIE_SPLITTER.split(value).forEach(cookieJoiner::add);
} else {
converted.add(name, convertHeaderValue(name, value));
}
}
if (cookieJoiner != null && cookieJoiner.length() != 0) {
converted.add(HttpHeaderNames.COOKIE, cookieJoiner.toString());
}
return converted;
}
/**
* Converts the headers of the given Netty HTTP/1.x request into Armeria HTTP/2 headers.
* The following headers are only used if they can not be found in the {@code HOST} header or the
* {@code Request-Line} as defined by rfc7230
*
* - {@link ExtensionHeaderNames#SCHEME}
*
* {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}.
*/
public static HttpHeaders toArmeria(HttpRequest in) throws URISyntaxException {
final URI requestTargetUri = toUri(in);
final io.netty.handler.codec.http.HttpHeaders inHeaders = in.headers();
final HttpHeaders out = new DefaultHttpHeaders(true, inHeaders.size());
out.path(toHttp2Path(requestTargetUri));
out.method(HttpMethod.valueOf(in.method().name()));
setHttp2Scheme(inHeaders, requestTargetUri, out);
if (!isOriginForm(requestTargetUri) && !isAsteriskForm(requestTargetUri)) {
// Attempt to take from HOST header before taking from the request-line
final String host = inHeaders.getAsString(HttpHeaderNames.HOST);
setHttp2Authority(host == null || host.isEmpty() ? requestTargetUri.getAuthority() : host, out);
}
// Add the HTTP headers which have not been consumed above
toArmeria(inHeaders, out);
return out;
}
/**
* Converts the headers of the given Netty HTTP/1.x response into Armeria HTTP/2 headers.
*/
public static HttpHeaders toArmeria(HttpResponse in) {
final io.netty.handler.codec.http.HttpHeaders inHeaders = in.headers();
final HttpHeaders out = new DefaultHttpHeaders(true, inHeaders.size());
out.status(in.status().code());
// Add the HTTP headers which have not been consumed above
toArmeria(inHeaders, out);
return out;
}
/**
* Converts the specified Netty HTTP/1 headers into Armeria HTTP/2 headers.
*/
public static HttpHeaders toArmeria(io.netty.handler.codec.http.HttpHeaders inHeaders) {
if (inHeaders.isEmpty()) {
return HttpHeaders.EMPTY_HEADERS;
}
final HttpHeaders out = new DefaultHttpHeaders(true, inHeaders.size());
toArmeria(inHeaders, out);
return out;
}
/**
* Converts the specified Netty HTTP/1 headers into Armeria HTTP/2 headers.
*/
public static void toArmeria(io.netty.handler.codec.http.HttpHeaders inHeaders, HttpHeaders out) {
final Iterator> iter = inHeaders.iteratorCharSequence();
// Choose 8 as a default size because it is unlikely we will see more than 4 Connection headers values,
// but still allowing for "enough" space in the map to reduce the chance of hash code collision.
final CharSequenceMap connectionBlacklist =
toLowercaseMap(inHeaders.valueCharSequenceIterator(HttpHeaderNames.CONNECTION), 8);
StringJoiner cookieJoiner = null;
while (iter.hasNext()) {
final Entry entry = iter.next();
final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
if (HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName) || connectionBlacklist.contains(aName)) {
continue;
}
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2 makes a special exception for TE
if (aName.equals(HttpHeaderNames.TE)) {
toHttp2HeadersFilterTE(entry, out);
continue;
}
// Cookies must be concatenated into a single octet string.
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5
final CharSequence value = entry.getValue();
if (aName.equals(HttpHeaderNames.COOKIE)) {
if (cookieJoiner == null) {
cookieJoiner = new StringJoiner(COOKIE_SEPARATOR);
}
COOKIE_SPLITTER.split(value).forEach(cookieJoiner::add);
} else {
out.add(aName, convertHeaderValue(aName, value));
}
}
if (cookieJoiner != null && cookieJoiner.length() != 0) {
out.add(HttpHeaderNames.COOKIE, cookieJoiner.toString());
}
}
private static CharSequenceMap toLowercaseMap(Iterator extends CharSequence> valuesIter,
int arraySizeHint) {
final CharSequenceMap result = new CharSequenceMap(arraySizeHint);
while (valuesIter.hasNext()) {
final AsciiString lowerCased = AsciiString.of(valuesIter.next()).toLowerCase();
try {
int index = lowerCased.forEachByte(FIND_COMMA);
if (index != -1) {
int start = 0;
do {
result.add(lowerCased.subSequence(start, index, false).trim(), EMPTY_STRING);
start = index + 1;
} while (start < lowerCased.length() &&
(index = lowerCased.forEachByte(start,
lowerCased.length() - start, FIND_COMMA)) != -1);
result.add(lowerCased.subSequence(start, lowerCased.length(), false).trim(), EMPTY_STRING);
} else {
result.add(lowerCased.trim(), EMPTY_STRING);
}
} catch (Exception e) {
// This is not expect to happen because FIND_COMMA never throws but must be caught
// because of the ByteProcessor interface.
throw new IllegalStateException(e);
}
}
return result;
}
/**
* Filter the {@link HttpHeaderNames#TE} header according to the
* special rules in the HTTP/2 RFC.
* @param entry An entry whose name is {@link HttpHeaderNames#TE}.
* @param out the resulting HTTP/2 headers.
*/
private static void toHttp2HeadersFilterTE(Entry entry,
HttpHeaders out) {
if (AsciiString.indexOf(entry.getValue(), ',', 0) == -1) {
if (AsciiString.contentEqualsIgnoreCase(AsciiString.trim(entry.getValue()),
HttpHeaderValues.TRAILERS)) {
out.add(HttpHeaderNames.TE, HttpHeaderValues.TRAILERS.toString());
}
} else {
final List teValues = StringUtil.unescapeCsvFields(entry.getValue());
for (CharSequence teValue : teValues) {
if (AsciiString.contentEqualsIgnoreCase(AsciiString.trim(teValue),
HttpHeaderValues.TRAILERS)) {
out.add(HttpHeaderNames.TE, HttpHeaderValues.TRAILERS.toString());
break;
}
}
}
}
private static URI toUri(HttpRequest in) throws URISyntaxException {
final String uri = in.uri();
if (uri.startsWith("//")) {
// Normalize the path that starts with more than one slash into the one with a single slash,
// so that java.net.URI does not raise a URISyntaxException.
for (int i = 0; i < uri.length(); i++) {
if (uri.charAt(i) != '/') {
return new URI(uri.substring(i - 1));
}
}
return ROOT;
} else {
return new URI(uri);
}
}
/**
* Generate a HTTP/2 {code :path} from a URI in accordance with
* rfc7230, 5.3.
*/
private static String toHttp2Path(URI uri) {
final StringBuilder pathBuilder = new StringBuilder(
length(uri.getRawPath()) + length(uri.getRawQuery()) + length(uri.getRawFragment()) + 2);
if (!isNullOrEmpty(uri.getRawPath())) {
pathBuilder.append(uri.getRawPath());
}
if (!isNullOrEmpty(uri.getRawQuery())) {
pathBuilder.append('?');
pathBuilder.append(uri.getRawQuery());
}
if (!isNullOrEmpty(uri.getRawFragment())) {
pathBuilder.append('#');
pathBuilder.append(uri.getRawFragment());
}
return pathBuilder.length() != 0 ? pathBuilder.toString() : EMPTY_REQUEST_PATH;
}
@VisibleForTesting
static void setHttp2Authority(@Nullable String authority, HttpHeaders out) {
// The authority MUST NOT include the deprecated "userinfo" subcomponent
if (authority != null) {
final String actualAuthority;
if (authority.isEmpty()) {
actualAuthority = "";
} else {
final int start = authority.indexOf('@') + 1;
if (start == 0) {
actualAuthority = authority;
} else if (authority.length() == start) {
throw new IllegalArgumentException("authority: " + authority);
} else {
actualAuthority = authority.substring(start);
}
}
out.authority(actualAuthority);
}
}
private static void setHttp2Scheme(io.netty.handler.codec.http.HttpHeaders in, URI uri, HttpHeaders out) {
final String value = uri.getScheme();
if (value != null) {
out.scheme(value);
return;
}
// Consume the Scheme extension header if present
final CharSequence cValue = in.get(ExtensionHeaderNames.SCHEME.text());
if (cValue != null) {
out.scheme(cValue.toString());
} else {
out.scheme("unknown");
}
}
/**
* Converts the specified Armeria HTTP/2 headers into Netty HTTP/2 headers.
*/
public static Http2Headers toNettyHttp2(HttpHeaders in) {
final Http2Headers out = new DefaultHttp2Headers(false, in.size());
out.set(in);
out.remove(HttpHeaderNames.CONNECTION);
out.remove(HttpHeaderNames.TRANSFER_ENCODING);
out.remove(HttpHeaderNames.TRAILER);
if (!out.contains(HttpHeaderNames.COOKIE)) {
return out;
}
// Split up cookies to allow for better compression.
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5
final List cookies = out.getAllAndRemove(HttpHeaderNames.COOKIE);
for (CharSequence c : cookies) {
out.add(HttpHeaderNames.COOKIE, COOKIE_SPLITTER.split(c));
}
return out;
}
/**
* Translate and add HTTP/2 headers to HTTP/1.x headers.
*
* @param streamId The stream associated with {@code sourceHeaders}.
* @param inputHeaders The HTTP/2 headers to convert.
* @param outputHeaders The object which will contain the resulting HTTP/1.x headers..
* @param httpVersion What HTTP/1.x version {@code outputHeaders} should be treated as
* when doing the conversion.
* @param isTrailer {@code true} if {@code outputHeaders} should be treated as trailing headers.
* {@code false} otherwise.
* @param isRequest {@code true} if the {@code outputHeaders} will be used in a request message.
* {@code false} for response message.
*
* @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x.
*/
public static void toNettyHttp1(
int streamId, HttpHeaders inputHeaders, io.netty.handler.codec.http.HttpHeaders outputHeaders,
HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception {
final CharSequenceMap translations = isRequest ? REQUEST_HEADER_TRANSLATIONS
: RESPONSE_HEADER_TRANSLATIONS;
StringJoiner cookieJoiner = null;
try {
for (Entry entry : inputHeaders) {
final AsciiString name = entry.getKey();
final String value = entry.getValue();
final AsciiString translatedName = translations.get(name);
if (translatedName != null) {
outputHeaders.add(translatedName, value);
continue;
}
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
if (name.isEmpty() || HTTP2_TO_HTTP_HEADER_BLACKLIST.contains(name)) {
continue;
}
if (HttpHeaderNames.COOKIE.equals(name)) {
// combine the cookie values into 1 header entry.
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5
if (cookieJoiner == null) {
cookieJoiner = new StringJoiner(COOKIE_SEPARATOR);
}
COOKIE_SPLITTER.split(value).forEach(cookieJoiner::add);
} else {
outputHeaders.add(name, value);
}
}
if (cookieJoiner != null && cookieJoiner.length() != 0) {
outputHeaders.add(HttpHeaderNames.COOKIE, cookieJoiner.toString());
}
} catch (Throwable t) {
throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
}
if (!isTrailer) {
HttpUtil.setKeepAlive(outputHeaders, httpVersion, true);
}
}
private static String convertHeaderValue(AsciiString name, CharSequence value) {
if (!(value instanceof AsciiString)) {
return value.toString();
}
if (HEADER_VALUE_CACHE != null && CACHED_HEADERS.contains(name)) {
final String converted = HEADER_VALUE_CACHE.get((AsciiString) value);
assert converted != null; // loader does not return null.
return converted;
}
return value.toString();
}
private static final class CharSequenceMap
extends DefaultHeaders {
CharSequenceMap() {
super(HTTP2_HEADER_NAME_HASHER, UnsupportedValueConverter.instance());
}
@SuppressWarnings("unchecked")
CharSequenceMap(int size) {
super(HTTP2_HEADER_NAME_HASHER, UnsupportedValueConverter.instance(), NameValidator.NOT_NULL, size);
}
}
private ArmeriaHttpUtil() {}
}