Skip to content

Commit 3d1001b

Browse files
committed
Add text/plain as default Accept header for logfile endpoint
closes codecentric#890
1 parent d86fd8d commit 3d1001b

File tree

5 files changed

+102
-25
lines changed

5 files changed

+102
-25
lines changed

spring-boot-admin-server-ui/src/main/frontend/utils/logtail.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default (getFn, interval, initialSize = 300 * 1024) => {
2222

2323
return timer(0, interval)
2424
.pipe(
25-
concatMap(() => getFn({headers: {range}})),
25+
concatMap(() => getFn({headers: {range, 'Accept': 'text/plain, */*'}})),
2626
concatMap(response => {
2727
const initial = size === 0;
2828
const contentLength = response.data.length;

spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoint.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
public class Endpoint implements Serializable {
2424
public static final String INFO = "info";
2525
public static final String HEALTH = "health";
26+
public static final String LOGFILE = "logfile";
2627
public static final String ENV = "env";
2728
public static final String HTTPTRACE = "httptrace";
2829
public static final String THREADDUMP = "threaddump";

spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/InstanceExchangeFilterFunctions.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
import reactor.core.publisher.Mono;
2525

2626
import java.net.URI;
27+
import java.util.List;
2728
import java.util.Optional;
2829
import java.util.function.Function;
2930
import org.slf4j.Logger;
3031
import org.slf4j.LoggerFactory;
32+
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
3133
import org.springframework.core.io.buffer.DataBuffer;
3234
import org.springframework.http.HttpHeaders;
3335
import org.springframework.http.MediaType;
@@ -40,13 +42,21 @@
4042

4143
import static de.codecentric.boot.admin.server.utils.MediaType.ACTUATOR_V1_MEDIATYPE;
4244
import static de.codecentric.boot.admin.server.utils.MediaType.ACTUATOR_V2_MEDIATYPE;
45+
import static java.util.Arrays.asList;
4346
import static java.util.Collections.singletonList;
4447
import static org.springframework.http.MediaType.APPLICATION_JSON;
4548

4649
public final class InstanceExchangeFilterFunctions {
4750
private static final Logger log = LoggerFactory.getLogger(InstanceExchangeFilterFunctions.class);
4851
public static final String ATTRIBUTE_INSTANCE = "instance";
4952
public static final String ATTRIBUTE_ENDPOINT = "endpointId";
53+
private static final List<MediaType> DEFAULT_ACCEPT_MEDIATYPES = asList(MediaType.parseMediaType(ActuatorMediaType.V2_JSON),
54+
MediaType.parseMediaType(ActuatorMediaType.V1_JSON),
55+
MediaType.APPLICATION_JSON
56+
);
57+
private static final List<MediaType> DEFAULT_LOGFILE_ACCEPT_MEDIATYPES = asList(MediaType.TEXT_PLAIN,
58+
MediaType.ALL
59+
);
5060

5161
private InstanceExchangeFilterFunctions() {
5262
}
@@ -56,8 +66,9 @@ public static ExchangeFilterFunction setInstance(Instance instance) {
5666
}
5767

5868
public static ExchangeFilterFunction setInstance(Mono<Instance> instance) {
59-
return (request, next) -> instance.map(
60-
i -> ClientRequest.from(request).attribute(ATTRIBUTE_INSTANCE, i).build())
69+
return (request, next) -> instance.map(i -> ClientRequest.from(request)
70+
.attribute(ATTRIBUTE_INSTANCE, i)
71+
.build())
6172
.switchIfEmpty(request.url().isAbsolute() ? Mono.just(request) : Mono.error(
6273
new ResolveInstanceException("Could not resolve Instance")))
6374
.flatMap(next::exchange);
@@ -66,8 +77,8 @@ public static ExchangeFilterFunction setInstance(Mono<Instance> instance) {
6677
public static ExchangeFilterFunction addHeaders(HttpHeadersProvider httpHeadersProvider) {
6778
return toExchangeFilterFunction((instance, request, next) -> {
6879
ClientRequest newRequest = ClientRequest.from(request)
69-
.headers(headers -> headers.addAll(
70-
httpHeadersProvider.getHeaders(instance)))
80+
.headers(headers -> headers.addAll(httpHeadersProvider.getHeaders(
81+
instance)))
7182
.build();
7283
return next.exchange(newRequest);
7384
});
@@ -103,8 +114,12 @@ public static ExchangeFilterFunction rewriteEndpointUrl() {
103114
}
104115

105116
URI newUrl = rewriteUrl(oldUrl, endpoint.get().getUrl());
106-
log.trace("URL '{}' for Endpoint {} of instance {} rewritten to {}", oldUrl, endpoint.get().getId(),
107-
instance.getId(), newUrl);
117+
log.trace("URL '{}' for Endpoint {} of instance {} rewritten to {}",
118+
oldUrl,
119+
endpoint.get().getId(),
120+
instance.getId(),
121+
newUrl
122+
);
108123
ClientRequest newRequest = ClientRequest.from(request)
109124
.attribute(ATTRIBUTE_ENDPOINT, endpoint.get().getId())
110125
.url(newUrl)
@@ -153,4 +168,19 @@ private static Function<ClientResponse, Mono<ClientResponse>> convertClientRespo
153168
return Mono.just(convertedResponse);
154169
};
155170
}
171+
172+
public static ExchangeFilterFunction setDefaultAcceptHeader() {
173+
return toExchangeFilterFunction((instance, request, next) -> {
174+
if (request.headers().getAccept().isEmpty()) {
175+
Boolean isRequestForLogfile = request.attribute(ATTRIBUTE_ENDPOINT)
176+
.map(Endpoint.LOGFILE::equals)
177+
.orElse(false);
178+
List<MediaType> acceptedHeaders = isRequestForLogfile ? DEFAULT_LOGFILE_ACCEPT_MEDIATYPES : DEFAULT_ACCEPT_MEDIATYPES;
179+
return next.exchange(ClientRequest.from(request)
180+
.headers(headers -> headers.setAccept(acceptedHeaders))
181+
.build());
182+
}
183+
return next.exchange(request);
184+
});
185+
}
156186
}

spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/InstanceWebClient.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@
2323

2424
import java.time.Duration;
2525
import java.util.concurrent.TimeUnit;
26-
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
2726
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
28-
import org.springframework.http.HttpHeaders;
29-
import org.springframework.http.MediaType;
3027
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
3128
import org.springframework.web.reactive.function.client.WebClient;
3229

@@ -52,6 +49,7 @@ public InstanceWebClient(WebClient webClient, HttpHeadersProvider httpHeadersPro
5249
this.webClient = webClient.mutate().filters(filters -> {
5350
filters.add(InstanceExchangeFilterFunctions.addHeaders(httpHeadersProvider));
5451
filters.add(InstanceExchangeFilterFunctions.rewriteEndpointUrl());
52+
filters.add(InstanceExchangeFilterFunctions.setDefaultAcceptHeader());
5553
filters.add(InstanceExchangeFilterFunctions.convertLegacyEndpoint(LegacyEndpointConverters.health()));
5654
filters.add(InstanceExchangeFilterFunctions.convertLegacyEndpoint(LegacyEndpointConverters.info()));
5755
filters.add(InstanceExchangeFilterFunctions.convertLegacyEndpoint(LegacyEndpointConverters.env()));
@@ -85,10 +83,7 @@ private static WebClient createDefaultWebClient(Duration connectTimeout,
8583
new ReadTimeoutHandler(readTimeout.toMillis(), TimeUnit.MILLISECONDS));
8684
}));
8785

88-
WebClient.Builder builder = WebClient.builder()
89-
.clientConnector(connector)
90-
.defaultHeader(HttpHeaders.ACCEPT, ActuatorMediaType.V2_JSON,
91-
ActuatorMediaType.V1_JSON, MediaType.APPLICATION_JSON_VALUE);
86+
WebClient.Builder builder = WebClient.builder().clientConnector(connector);
9287
customizer.customize(builder);
9388
return builder.build();
9489
}

spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/client/InstanceWebClientTest.java

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package de.codecentric.boot.admin.server.web.client;
1818

1919
import de.codecentric.boot.admin.server.domain.entities.Instance;
20+
import de.codecentric.boot.admin.server.domain.values.Endpoints;
2021
import de.codecentric.boot.admin.server.domain.values.InstanceId;
2122
import de.codecentric.boot.admin.server.domain.values.Registration;
2223
import de.codecentric.boot.admin.server.web.client.exception.ResolveEndpointException;
@@ -37,6 +38,7 @@
3738
import com.github.tomakehurst.wiremock.core.Options;
3839
import com.github.tomakehurst.wiremock.junit.WireMockClassRule;
3940

41+
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
4042
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
4143
import static com.github.tomakehurst.wiremock.client.WireMock.get;
4244
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
@@ -46,11 +48,11 @@
4648
import static org.assertj.core.api.Assertions.assertThat;
4749
import static org.mockito.Mockito.mock;
4850
import static org.mockito.Mockito.when;
49-
import static org.springframework.http.HttpHeaders.ACCEPT;
5051
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
5152
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
5253
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
5354
import static org.springframework.http.HttpHeaders.EMPTY;
55+
import static wiremock.org.apache.http.HttpHeaders.ACCEPT;
5456

5557
public class InstanceWebClientTest {
5658
@ClassRule
@@ -70,10 +72,7 @@ public void should_rewirte_url() {
7072
Mono<ClientResponse> exchange = instanceWebClient.instance(instance).get().uri("health").exchange();
7173

7274
StepVerifier.create(exchange).expectNextCount(1).verifyComplete();
73-
wireMock.verify(1,
74-
getRequestedFor(urlEqualTo("/status")).withHeader(ACCEPT, equalTo(MediaType.APPLICATION_JSON_VALUE))
75-
.withHeader(ACCEPT, equalTo(ActuatorMediaType.V1_JSON))
76-
.withHeader(ACCEPT, equalTo(ActuatorMediaType.V2_JSON)));
75+
wireMock.verify(1, getRequestedFor(urlEqualTo("/status")));
7776
}
7877

7978
@Test
@@ -104,7 +103,7 @@ public void should_exchange_absolute_url_without_instance() {
104103
}
105104

106105
@Test
107-
public void should_add_headers() {
106+
public void should_add_headers_from_provider() {
108107
Instance instance = Instance.create(InstanceId.of("id"))
109108
.register(Registration.create("test", wireMock.url("/status")).build());
110109
wireMock.stubFor(get("/status").willReturn(ok()));
@@ -116,15 +115,65 @@ public void should_add_headers() {
116115
wireMock.verify(1, getRequestedFor(urlEqualTo("/status")).withHeader(AUTHORIZATION, equalTo("streng:geheim")));
117116
}
118117

118+
@Test
119+
public void should_add_default_accept_headers() {
120+
Instance instance = Instance.create(InstanceId.of("id"))
121+
.register(Registration.create("test", wireMock.url("/status")).build());
122+
wireMock.stubFor(get("/status").willReturn(ok()));
123+
124+
Mono<ClientResponse> exchange = instanceWebClient.instance(instance).get().uri("health").exchange();
125+
126+
StepVerifier.create(exchange).expectNextCount(1).verifyComplete();
127+
wireMock.verify(1,
128+
getRequestedFor(urlEqualTo("/status")).withHeader(ACCEPT, containing(MediaType.APPLICATION_JSON_VALUE))
129+
.withHeader(ACCEPT, containing(ActuatorMediaType.V1_JSON))
130+
.withHeader(ACCEPT, containing(ActuatorMediaType.V2_JSON))
131+
);
132+
}
133+
134+
@Test
135+
public void should_not_add_default_accept_headers() {
136+
Instance instance = Instance.create(InstanceId.of("id"))
137+
.register(Registration.create("test", wireMock.url("/status")).build());
138+
wireMock.stubFor(get("/status").willReturn(ok()));
139+
140+
Mono<ClientResponse> exchange = instanceWebClient.instance(instance)
141+
.get()
142+
.uri("health")
143+
.header(ACCEPT, MediaType.TEXT_XML_VALUE)
144+
.exchange();
145+
146+
StepVerifier.create(exchange).expectNextCount(1).verifyComplete();
147+
wireMock.verify(1,
148+
getRequestedFor(urlEqualTo("/status")).withHeader(ACCEPT, equalTo(MediaType.TEXT_XML_VALUE))
149+
);
150+
}
151+
152+
@Test
153+
public void should_add_default_logfile_accept_headers() {
154+
Instance instance = Instance.create(InstanceId.of("id"))
155+
.register(Registration.create("test", wireMock.url("/status")).build())
156+
.withEndpoints(Endpoints.single("logfile", wireMock.url("/log")));
157+
wireMock.stubFor(get("/log").willReturn(ok()));
158+
159+
Mono<ClientResponse> exchange = instanceWebClient.instance(instance).get().uri("logfile").exchange();
160+
161+
StepVerifier.create(exchange).expectNextCount(1).verifyComplete();
162+
wireMock.verify(1,
163+
getRequestedFor(urlEqualTo("/log")).withHeader(ACCEPT, containing(MediaType.TEXT_PLAIN_VALUE))
164+
.withHeader(ACCEPT, containing(MediaType.ALL_VALUE))
165+
);
166+
}
167+
119168
@Test
120169
public void should_convert_legacy_endpont() {
121170
Instance instance = Instance.create(InstanceId.of("id"))
122171
.register(Registration.create("test", wireMock.url("/status")).build());
123172

124173
String responseBody = "{ \"status\" : \"UP\", \"foo\" : \"bar\" }";
125-
wireMock.stubFor(get("/status").willReturn(
126-
okForContentType(ActuatorMediaType.V1_JSON, responseBody).withHeader(CONTENT_LENGTH,
127-
Integer.toString(responseBody.length())).withHeader("X-Custom", "1234")));
174+
wireMock.stubFor(get("/status").willReturn(okForContentType(ActuatorMediaType.V1_JSON, responseBody).withHeader(CONTENT_LENGTH,
175+
Integer.toString(responseBody.length())
176+
).withHeader("X-Custom", "1234")));
128177

129178
Mono<ClientResponse> exchange = instanceWebClient.instance(instance).get().uri("health").exchange();
130179

@@ -183,8 +232,10 @@ public void should_error_on_missing_endpoint() {
183232

184233
@Test
185234
public void should_error_on_timeout() {
186-
InstanceWebClient fastTimeoutClient = new InstanceWebClient(headersProvider, Duration.ofMillis(10),
187-
Duration.ofMillis(10));
235+
InstanceWebClient fastTimeoutClient = new InstanceWebClient(headersProvider,
236+
Duration.ofMillis(10),
237+
Duration.ofMillis(10)
238+
);
188239

189240
wireMock.stubFor(get("/foo").willReturn(ok().withFixedDelay(100)));
190241

0 commit comments

Comments
 (0)