Skip to content

Commit

Permalink
Polish HttpClient/HttpServer cookies API (#2525)
Browse files Browse the repository at this point in the history
- `HttpClientInfos#cookies` works with `HttpSetCookie` and represents `Set-Cookie`
headers found in response headers
- `HttpClientRequest#addCookie` works with `HttpCookiePair` and represents `Cookie`
headers for request headers
- `HttpServerInfos#cookies/allCookies` works with `HttpCookiePair` and represents `Cookie`
headers found in request headers
- `HttpServerResponse#addCookie` works with `HttpSetCookie` and represents `Set-Cookie`
headers for response headers

Co-authored-by: Dariusz Jędrzejczyk <jdariusz@vmware.com>
  • Loading branch information
violetagg and chemicL committed Oct 3, 2022
1 parent 2f5fa9b commit 845badb
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 105 deletions.
67 changes: 4 additions & 63 deletions reactor-netty5-http/src/main/java/reactor/netty5/http/Cookies.java
Expand Up @@ -15,93 +15,34 @@
*/
package reactor.netty5.http;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

import io.netty5.handler.codec.http.headers.HttpCookiePair;
import io.netty5.handler.codec.http.headers.HttpHeaders;
import io.netty5.handler.codec.http.headers.HttpSetCookie;
import reactor.util.Logger;
import reactor.util.Loggers;

/**
* Holder for Set-Cookie headers found from response headers
* Base holder for Set-Cookie/Cookie headers found in response headers/request headers
*
* @since 0.6
*/
public class Cookies {

/**
* Return a new cookies holder from client response headers.
*
* @param headers client response headers
* @return a new cookies holder from client response headers
*/
public static Cookies newClientResponseHolder(HttpHeaders headers) {
return new Cookies(headers);
}
public abstract class Cookies<T extends HttpCookiePair> {

final static int NOT_READ = 0;
final static int READING = 1;
final static int READ = 2;

protected final HttpHeaders nettyHeaders;

protected Map<CharSequence, Set<HttpCookiePair>> cachedCookies;

volatile int state;
@SuppressWarnings("rawtypes")
static final AtomicIntegerFieldUpdater<Cookies> STATE =
AtomicIntegerFieldUpdater.newUpdater(Cookies.class, "state");
static final Logger log = Loggers.getLogger(Cookies.class);

protected Cookies(HttpHeaders nettyHeaders) {
this.nettyHeaders = Objects.requireNonNull(nettyHeaders, "nettyHeaders");
cachedCookies = Collections.emptyMap();
}

/**
* Wait for the cookies to become available, cache them and subsequently return the cached map of cookies.
*
* @return the cached map of cookies
*/
public Map<CharSequence, Set<HttpCookiePair>> getCachedCookies() {
if (!markReadingCookies()) {
for (;;) {
if (hasReadCookies()) {
return cachedCookies;
}
}
}

Map<CharSequence, Set<HttpCookiePair>> cookies = new HashMap<>();
Iterator<HttpSetCookie> setCookieItr = nettyHeaders.getSetCookiesIterator();
int setCookieIndex = -1;
while (setCookieItr.hasNext()) {
try {
setCookieIndex++;
HttpSetCookie setCookie = setCookieItr.next();
Set<HttpCookiePair> existingCookiesOfName = cookies.computeIfAbsent(setCookie.name(), k -> new HashSet<>());
existingCookiesOfName.add(setCookie);
}
catch (IllegalArgumentException err) {
// Ignore invalid syntax or whatever decoding error for the current Set-Cookie header.
// Since we don't know which Set-Cookie header is not parsed, we log the Set-Cookie header index number.
if (log.isDebugEnabled()) {
log.debug("Ignoring invalid Set-Cookie header (header index #{}) : {}", setCookieIndex, err.toString());
}
}
}

cachedCookies = Collections.unmodifiableMap(cookies);
markReadCookies();
return cachedCookies;
}
public abstract Map<CharSequence, Set<T>> getCachedCookies();

protected final boolean hasReadCookies() {
return state == READ;
Expand Down
Expand Up @@ -15,12 +15,8 @@
*/
package reactor.netty5.http;

import java.util.Map;
import java.util.Set;

import io.netty5.handler.codec.http.HttpMethod;
import io.netty5.handler.codec.http.HttpVersion;
import io.netty5.handler.codec.http.headers.HttpCookiePair;

/**
* An Http Reactive Channel with several accessors related to HTTP flow: headers, params,
Expand All @@ -31,13 +27,6 @@
*/
public interface HttpInfos {

/**
* Returns resolved HTTP cookies.
*
* @return Resolved HTTP cookies
*/
Map<CharSequence, Set<HttpCookiePair>> cookies();

/**
* Returns the decoded path portion from the {@link #uri()}
*
Expand Down
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2022 VMware, Inc. or its affiliates, All Rights Reserved.
*
* 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
*
* 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 reactor.netty5.http.client;

import io.netty5.handler.codec.http.headers.HttpHeaders;
import io.netty5.handler.codec.http.headers.HttpSetCookie;
import reactor.netty5.http.Cookies;
import reactor.util.Logger;
import reactor.util.Loggers;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
* Holder for Set-Cookie headers found in response headers
*
* @author Violeta Georgieva
* @since 2.0.0
*/
public final class ClientCookies extends Cookies<HttpSetCookie> {
static final Logger log = Loggers.getLogger(Cookies.class);

/**
* Return a new cookies holder from client response headers.
*
* @param headers client response headers
* @return a new cookies holder from client response headers
*/
public static ClientCookies newClientResponseHolder(HttpHeaders headers) {
return new ClientCookies(headers);
}

final HttpHeaders nettyHeaders;

Map<CharSequence, Set<HttpSetCookie>> cachedCookies;

ClientCookies(HttpHeaders nettyHeaders) {
this.cachedCookies = Collections.emptyMap();
this.nettyHeaders = Objects.requireNonNull(nettyHeaders, "nettyHeaders");
}

@Override
public Map<CharSequence, Set<HttpSetCookie>> getCachedCookies() {
if (!markReadingCookies()) {
for (;;) {
if (hasReadCookies()) {
return cachedCookies;
}
}
}

Map<CharSequence, Set<HttpSetCookie>> cookies = new HashMap<>();
Iterator<HttpSetCookie> setCookieItr = nettyHeaders.getSetCookiesIterator();
int setCookieIndex = -1;
while (setCookieItr.hasNext()) {
try {
setCookieIndex++;
HttpSetCookie setCookie = setCookieItr.next();
Set<HttpSetCookie> existingCookiesOfName = cookies.computeIfAbsent(setCookie.name(), k -> new HashSet<>());
existingCookiesOfName.add(setCookie);
}
catch (IllegalArgumentException err) {
// Ignore invalid syntax or whatever decoding error for the current Set-Cookie header.
// Since we don't know which Set-Cookie header is not parsed, we log the Set-Cookie header index number.
if (log.isDebugEnabled()) {
log.debug("Ignoring invalid Set-Cookie header (header index #{}) : {}", setCookieIndex, err.toString());
}
}
}

cachedCookies = Collections.unmodifiableMap(cookies);
markReadCookies();
return cachedCookies;
}
}
Expand Up @@ -21,7 +21,7 @@
import io.netty5.handler.codec.http.HttpMethod;
import io.netty5.handler.codec.http.HttpVersion;
import io.netty5.handler.codec.http.headers.HttpCookiePair;
import reactor.netty5.http.Cookies;
import io.netty5.handler.codec.http.headers.HttpSetCookie;
import reactor.netty5.http.HttpOperations;
import reactor.util.context.ContextView;

Expand Down Expand Up @@ -67,8 +67,8 @@ public HttpClientRequest addHeader(CharSequence name, CharSequence value) {
}

@Override
public Map<CharSequence, Set<HttpCookiePair>> cookies() {
return Cookies.newClientResponseHolder(headers)
public Map<CharSequence, Set<HttpSetCookie>> cookies() {
return ClientCookies.newClientResponseHolder(headers)
.getCachedCookies();
}

Expand Down
Expand Up @@ -16,10 +16,14 @@
package reactor.netty5.http.client;

import io.netty5.handler.codec.http.headers.HttpHeaders;
import io.netty5.handler.codec.http.headers.HttpSetCookie;
import reactor.netty5.http.HttpInfos;
import reactor.util.annotation.Nullable;
import reactor.util.context.ContextView;

import java.util.Map;
import java.util.Set;

/**
* An Http Reactive Channel with several accessors related to HTTP flow: resource URL,
* information for redirections etc...
Expand All @@ -28,6 +32,13 @@
*/
public interface HttpClientInfos extends HttpInfos {

/**
* Returns resolved HTTP cookies.
*
* @return Resolved HTTP cookies
*/
Map<CharSequence, Set<HttpSetCookie>> cookies();

/**
* Return the current {@link ContextView} associated with the Mono/Flux exposed
* via {@link HttpClient.ResponseReceiver#response()} or related terminating API.
Expand Down
Expand Up @@ -54,6 +54,7 @@
import io.netty5.handler.codec.http.HttpUtil;
import io.netty5.handler.codec.http.HttpVersion;
import io.netty5.handler.codec.http.LastHttpContent;
import io.netty5.handler.codec.http.headers.HttpSetCookie;
import io.netty5.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
import io.netty5.handler.timeout.ReadTimeoutHandler;
import io.netty5.util.Resource;
Expand All @@ -69,7 +70,6 @@
import reactor.netty5.NettyPipeline;
import reactor.netty5.channel.AbortedException;
import reactor.netty5.channel.ChannelOperations;
import reactor.netty5.http.Cookies;
import reactor.netty5.http.HttpOperations;
import reactor.util.Logger;
import reactor.util.Loggers;
Expand Down Expand Up @@ -217,7 +217,7 @@ public HttpClientOperations withConnection(Consumer<? super Connection> withConn
}

@Override
public Map<CharSequence, Set<HttpCookiePair>> cookies() {
public Map<CharSequence, Set<HttpSetCookie>> cookies() {
ResponseState responseState = this.responseState;
if (responseState != null && responseState.cookieHolder != null) {
return responseState.cookieHolder.getCachedCookies();
Expand Down Expand Up @@ -773,14 +773,14 @@ final void withWebsocketSupport(WebsocketClientSpec websocketClientSpec) {

static final class ResponseState {

final HttpResponse response;
final HttpHeaders headers;
final Cookies cookieHolder;
final HttpResponse response;
final HttpHeaders headers;
final ClientCookies cookieHolder;

ResponseState(HttpResponse response, HttpHeaders headers) {
this.response = response;
this.headers = headers;
this.cookieHolder = Cookies.newClientResponseHolder(headers);
this.cookieHolder = ClientCookies.newClientResponseHolder(headers);
}
}

Expand Down
Expand Up @@ -20,6 +20,7 @@

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* An Http Reactive Channel with several accessors related to HTTP flow: headers, params,
Expand All @@ -30,6 +31,13 @@
*/
public interface HttpServerInfos extends HttpInfos {

/**
* Returns resolved HTTP cookies.
*
* @return Resolved HTTP cookies
*/
Map<CharSequence, Set<HttpCookiePair>> cookies();

/**
* Returns resolved HTTP cookies. As opposed to {@link #cookies()}, this
* returns all cookies, even if they have the same name.
Expand Down
Expand Up @@ -25,15 +25,16 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
* Holder for Cookie headers found from request headers
* Holder for Cookie headers found in request headers
*
* @author Violeta Georgieva
* @since 1.0.8
*/
public final class ServerCookies extends Cookies {
public final class ServerCookies extends Cookies<HttpCookiePair> {

/**
* Return a new cookies holder from server request headers.
Expand All @@ -45,11 +46,15 @@ public static ServerCookies newServerRequestHolder(HttpHeaders headers) {
return new ServerCookies(headers);
}

final HttpHeaders nettyHeaders;

Map<CharSequence, List<HttpCookiePair>> allCachedCookies;
Map<CharSequence, Set<HttpCookiePair>> cachedCookies;

ServerCookies(HttpHeaders nettyHeaders) {
super(nettyHeaders);
allCachedCookies = Collections.emptyMap();
this.allCachedCookies = Collections.emptyMap();
this.cachedCookies = Collections.emptyMap();
this.nettyHeaders = Objects.requireNonNull(nettyHeaders, "nettyHeaders");
}

@Override
Expand Down
Expand Up @@ -26,7 +26,7 @@
import io.netty5.handler.codec.http.LastHttpContent;
import io.netty5.util.concurrent.Future;
import reactor.netty5.channel.ChannelOperations;
import reactor.netty5.http.HttpInfos;
import reactor.netty5.http.server.HttpServerInfos;
import reactor.util.annotation.Nullable;

import java.util.function.Function;
Expand Down Expand Up @@ -74,7 +74,7 @@ public Future<Void> write(ChannelHandlerContext ctx, Object msg) {
}

ChannelOperations<?, ?> ops = ChannelOperations.get(ctx.channel());
if (ops instanceof HttpInfos httpInfos) {
if (ops instanceof HttpServerInfos httpInfos) {
accessLogArgProvider.cookies(httpInfos.cookies());
}
}
Expand Down

0 comments on commit 845badb

Please sign in to comment.