GetMapping, produces and HttpMediaTypeNotAcceptableException - spring-boot

I am trying to add versioning of my rest endpoints. I was following the tutorial found at https://www.springboottutorial.com/spring-boot-versioning-for-rest-services. I am using spring-boot-starter-parent 2.6.2. I created 2 endpoints:
#GetMapping(value="/student/produces", produces="application/vnd.app-v1+json")
public StudentV1 producesV1() {
return new StudentV1("AA");
}
#GetMapping(value="/student/produces", produces="application/vnd.app-v2+json")
public StudentV2 producesV2() {
return new StudentV1(new Name("BB"));
}
Whenever I try to hit the endpoints (either via swagger/OAS3 or curl) I get an HttpMediaTypeNotAcceptableException: Could not find acceptable representation. None of my google-fu has helped.
Any thoughts?

Fixed it. In our WebMvcConfigurer.configureContentNegotiation we had set contentNegotiationConfigurer.ignoreAcceptHeader to true. Changed it to false and all is good.

Related

How to combine sink.asFlux() with Server-Sent Events (SSE) using Spring WebFlux?

I am using Spring Boot 2.7.8 with WebFlux.
I have a sink in my class like this:
private final Sinks.Many<TaskEvent> sink = Sinks.many()
.multicast()
.onBackpressureBuffer();
This can be used to subscribe on like this:
public Flux<List<TaskEvent>> subscribeToTaskUpdates() {
return sink.asFlux()
.buffer(Duration.ofSeconds(1))
.share();
}
The #Controller uses this like this to push the updates as a Server-Sent Event (SSE) to the browser:
#GetMapping("/transferdatestatuses/updates")
public Flux<ServerSentEvent<TransferDateStatusesUpdateEvent>> subscribeToTransferDataStatusUpdates() {
return monitoringSseBroker.subscribeToTaskUpdates()
.map(taskEventList -> ServerSentEvent.<TransferDateStatusesUpdateEvent>builder()
.data(TransferDateStatusesUpdateEvent.of(taskEventList))
.build())
This works fine at first, but if I navigate away in my (Thymeleaf) web application to a page that has no connection with the SSE url and then go back, then the browser cannot connect anymore.
After some investigation, I found out that the problem is that the removal of the subscriber closes the flux and a new subscriber cannot connect anymore.
I have found 3 ways to fix it, but I don't understand the internals enough to decide which one is the best solution and if there any things I need to consider to decide what to use.
Solution 1
Disable the autoCancel on the sink by using the method overload of onBackpressureBuffer that allows to set this parameter:
private final Sinks.Many<TaskEvent> sink = Sinks.many()
.multicast()
.onBackpressureBuffer(Queues.SMALL_BUFFER_SIZE, false);
Solution 2
Use replay(0).autoConnect() instead of share():
public Flux<List<TaskEvent>> subscribeToTaskUpdates() {
return sink.asFlux()
.buffer(Duration.ofSeconds(1))
.replay(0).autoConnect();
}
Solution 3
Use publish().autoConnect() instead of share():
public Flux<List<TaskEvent>> subscribeToTaskUpdates() {
return sink.asFlux()
.buffer(Duration.ofSeconds(1))
.publish().autoConnect();
}
Which of the solutions are advisable to make sure a browser can disconnect and connect again later without problems?
I'm not quite sure if it is the root of your problem, but I didn't have that issue by using a keepAlive Flux.
val keepAlive = Flux.interval(Duration.ofSeconds(10)).map {
ServerSentEvent.builder<Image>()
.event(":keepalive")
.build()
}
return Flux.merge(
keepAlive,
imageUpdateFlux
)
Here is the whole file: Github

Multiple swagger documents with Spring Boot - for different http operations

I figured that there is a way to group endpoints into different swagger documents, but i wanted to know if there is a way to separate endpoints based on get/post/patch operations?
For eg, i have 2 endpoints
Get : /app/employee
Post: /app/employee
How can i segregate them into 2 different swagger documents?
Edit 1 : I am referring the below article to segregate swagger endpoints in spring boot:
https://dev.to/s2agrahari/grouping-apis-in-swagger-55kk
Maybe your questione answered by using tags?
Here is the Official example.
Regards.
In your application.yaml, you can configure like this:
springdoc:
swagger-ui:
operationsSorter: method
enabled: true
tags-sorter: alpha
Reference: https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
Also there is a question with more details for setting it programatically: https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
You gave the answer partially yourself, you can just do:
#Bean
GroupedOpenApi getApis() {
return GroupedOpenApi.builder().group("My Get Apis").addOpenApiCustomiser(customizer()).build();
}
And then some logic (maybe you have double check it, but something like so):
public OpenApiCustomiser customizer() {
return (OpenAPI openApi) -> {
final List<String> keysToDelete = new ArrayList<>();
for (Map.Entry<String, PathItem> entry : openApi.getPaths().entrySet()) {
if(entry.getValue().getGet() == null) {
keysToDelete.add(entry.getKey());
}
}
keysToDelete.forEach(key -> openApi.getPaths().remove(key));
};
}
And the same for Post or any other Rest operation....

Customizing Apache Camels ExchangeFormatter using Spring-Boot

by default i assume that spring-boot/camel is using org.apache.camel.support.processor.DefaultExchangeFormatter.
I wonder how I can set the flag 'showHeaders' inside a spring-boot app.
Because I hope to see the headers in the "org.apache.camel.tracing" log as well.
Wish all a wonderful day
DefaultTracer is used in Camel to trace routes by default.
It is created with showHeaders(false) formatter option set.
Therefore you could implement another Tracer (consider extending from DefaultTracer) to enable putting headers into traced messages.
i need this mostly in my tests. so i have built this into my basic test class
#BeforeEach
public void before() {
if( camelContext.getTracer().getExchangeFormatter() instanceof DefaultExchangeFormatter ) {
DefaultExchangeFormatter def = (DefaultExchangeFormatter)camelContext.getTracer().getExchangeFormatter();
def.setShowHeaders(true);
}
}

Renamed: CSRF with Angular and Spring ("Request method 'DELETE' not supported")

I am trying to call my controller's delete method:
Spring:
#RestController
#RequestMapping("/api")
#CrossOrigin
public class Controller
{
#DeleteMapping("thing/item/{name}")
public void deleteThing(#PathVariable String name)
{
System.out.println("CALL"); // Never called!!!
}
}
Angular 7:
deleteTemplate(name: string) {
const url = `host/api/thing/item/${name}`;
return this.http.delete(url);
}
I've found something about including options:
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
deleteTemplate(name: string) {
const url = `${host}/api/thing/item/${name}`; // host is not relevant, same path with GET works.
return this.http.delete(url, this.httpOptions);
}
But I don't think that it's even needed since everything I am sending is in link itself (no json).
Every time its:
WARN 15280 --- [io-8443-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'DELETE' not supported]
How to fix this?
DELETE is sent with 405 error and preceeding OPTIONS (with 200 code). It reaches server and does this error.
EDIT
Above is a sample of code that is simplified and still doesn't work. I understand your comments, but host can be anything and doesn't matter here. Its localhost now and as mentioned - it works in sense of reaching endpoint.
To check if I am wrong I did this:
#Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
this.requestMappingHandlerMapping.getHandlerMethods()
.forEach((e, k) -> System.out.println(e + " OF " + k));
After startup, it printed everything I expected, including:
{GET /api/thing/item/{name}} OF public myPackage.Thing myPackage.Controller.getThing(java.lang.String)
{DELETE /api/thing/item/{name}} OF public void myPackage.Controller.deleteThing(java.lang.String)
Any ideas?
Goddamn. Turns out problem was in CSRF, mainly in hidden Angular documentation and misleading Spring filter logs.
It took me about 10h to make it work, but only in production (where Angular is on same host/port with Spring). I will try to make it work in dev, but that requires basically rewriting whole module supplied by Angular that is bound to its defaults.
Now as to the problem:
Everything starts with Spring Security when we want to use CSRF (and we do). Apparently CsrfFilter literally explodes if it can't match CSRF tokens it expects and falls back to /error. This all happens without ANY specific log message, but simple Request method 'DELETE' not supported, which from my question we know IT IS present.
This is not only for DELETE, but all action requests (non-GET/HEAD).
This all points to "How to setup CSRF with Angular?". Well, here we go:
https://angular.io/guide/http#security-xsrf-protection
And it WORKS by DEFAULT. Based on Cookie-Header mechanism for CSRF.
But wait, there's more!
Spring needs to bo configured for Cookie-Header mechanism. Happily we got:
#Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().csrfTokenRepository(this.csrfRepo());
}
private CookieCsrfTokenRepository csrfRepo()
{
CookieCsrfTokenRepository repo = new CookieCsrfTokenRepository();
repo.setCookieHttpOnly(false);
return repo;
}
Which is built-in in spring and actually uses same cookie/header names as ones used by Angular. But what's up with repo.setCookieHttpOnly(false);? Well... another poor documented thingy. You just NEED to set it to false, otherwise Angular's side will not work.
Again - this only works for production builds, because Angular doesn't support external links. More at: Angular 6 does not add X-XSRF-TOKEN header to http request
So yeah... to make it work in dev localhost with 2 servers I'll need 1st to recreate https://angular.io/api/common/http/HttpClientXsrfModule mechanism to also intercept non-default requests.
EDIT
Based on JB Nizet comment I cooked up setup that works for both dev and production. Indeed proxy is awesome. With CSRF and SSL enabled (in my case), you also need to enable SSL on Angular CLI, otherwise Angular will not be able to use CSRF of SSL-enabled proxy backend and thus not work.
"serve": {
"options": {
"proxyConfig": "proxy.conf.json",
"sslKey": "localhost.key",
"sslCert": "localhost.crt",
"ssl": true
}
}
Note that Cert and Key will be auto-generated if not found :)
For Spring backend you don't need same cert. I use separate JKS there.
Cheers!
Add a slash / at the beginning of your path.
#DeleteMapping("/thing/item/{name}")

How to get Method extensions from gRPC

I am using interceptors to perform additional validation based on the optional extensions set on an RPC on incoming and out going RPCs.
Given the following gRPC schema:
extend google.protobuf.MethodOptions {
string my_option = 50006;
}
service MyService {
rpc Foo (FooRequest) returns (FooResponse) {
option (my_option) = "foo"
}
}
How do I go about getting the value of my_option? At first I had thought to get it from the request using this. However, as this is a MethodOptions it doesn't seem that its part of the descriptor. Thoughts?
Found the following answer for those who get here in the future.

Resources