In my development environment I'm using two separate technology stacks for the frontend and the backend:
- React + webpack-dev-server for the frontend, served at
localhost:8081
- Spring for the backend, serving HTTP endpoints over
localhost:7080
, in two ways:- using integration for the data processing (reception, transformation and storage), this processing starts at an
http:inbound-channel-adapter
(see the following XML configuration) - using MVC to provide a read-only (i.e. GET-only) access through REST APIs (implemented with
@RestController
-annotated classes)
- using integration for the data processing (reception, transformation and storage), this processing starts at an
The frontend interacts with the backend using only fetch()
calls.
Being on a different port, the browser blocks any request to the HTTP endpoints without a proper CORS configuration. Since this is going to be temporary, I tried to come up with the easiest / fastest solution, I managed to make the REST API calls work, but the inbound adapter still gives troubles during the OPTIONS request.
The webapp has two sections:
- a table that pulls data from the REST API
- this part works, the frontend successfully retrieves data from the backend
- a form that uploads a file with a couple of other fields
- here I keep hitting errors
This is the current Spring Integration configuration (I'm using version 5.0.3):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ctx="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-file="http://www.springframework.org/schema/integration/file"
xmlns:int-jdbc="http://www.springframework.org/schema/integration/jdbc"
xmlns:int-sftp="http://www.springframework.org/schema/integration/sftp"
xmlns:int-http="http://www.springframework.org/schema/integration/http"
xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="...">
<!-- ... -->
<int-http:inbound-channel-adapter id="httpInboundAdapter"
channel="httpRequestsChannel"
path="/services/requests"
supported-methods="POST">
<int-http:cross-origin allow-credentials="false" allowed-headers="*" origin="*" />
</int-http:inbound-channel-adapter>
<int:channel id="httpRequestsChannel" />
<int:chain input-channel="httpRequestsChannel" id="httpRequestsChain">
<int:transformer method="transform">
<bean class="transformers.RequestToMessageFile" />
</int:transformer>
<!-- ... -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" />
<!-- ... -->
</beans>
This instead is how one of the REST endpoints is done:
package api;
// imports
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping(path="/api/requests", produces="application/json")
public class RequestController {
// ...
Here's the transformer that should handle the POSTed file:
package transformers;
// imports
public class RequestToMessageFile {
private static final String ALLOW_DUPES = "allowDuplicateFilenames";
public Message<?> transform(LinkedMultiValueMap<String, Object> multipartRequest) {
File file = null;
int tx = -1;
String localFilename = null;
for (String elementName : multipartRequest.keySet()) {
if (RequestFields.TRANSACTION_TYPE.equals(elementName)) {
tx = Integer.valueOf(((String[]) multipartRequest.getFirst(RequestFields.TRANSACTION_TYPE))[0]);
} else if (RequestFields.FILE.equals(elementName)){
try {
UploadedMultipartFile uploadedFile = (UploadedMultipartFile) multipartRequest.getFirst(RequestFields.FILE);
// file handling
file = new File(/* ... */);
uploadedFile.transferTo(file);
} catch (IllegalStateException e) {
LogManager.getLogger(getClass()).error(e, e);
} catch (IOException e) {
LogManager.getLogger(getClass()).error(e, e);
}
}
}
if(file != null) {
MessageBuilder<File> builder = MessageBuilder.withPayload(file);
// ...
Message<?> m = builder.build();
return m;
} else {
return MessageBuilder.withPayload(new Object()).build();
}
}
}
This is how I'm calling the endpoint from the browser:
var data = new FormData();
data.append('transaction-type', this.state.transactionType);
data.append('file', this.state.file);
fetch('http://localhost:7080/services/requests?allowDuplicateFilenames=true', {
method: 'POST',
body: data,
mode: 'cors',
headers:{
'Accept': 'application/json',
},
}
)
.then(/* ... */)
.catch(/* ... */)
What I'm experiencing is that the transformer is invoked during the OPTIONS
request (which I wasn't expecting), of course carrying no body thus only the allowDuplicateFilenames
url parameter is present.
Since the file can't be determined, the transformer is returning a message withPayload(new Object())
, but the request keeps going forward on the configured channels and endpoints, eventually ending in an error when empty data is passed to the last endpoint — a jdbc:outbound-channel-adapter
.
The error ultimately returns to the client as a HTTP 500 error.
I would expect that only the POST
request would be passed to the http inbound adapter, while the OPTIONS
one is handled by the framework, but that may be a wrong interpretation.
What's preventing POST requests from succeed? Is there something wrong in my configuration? Should I handle the OPTIONS
request too?
In subsequent tries, I've added the Allow-Control-Allow-Origin: *
extension to Chrome and removed the headers
parameter from the frontend fetch()
calls. With this setting the requests work, because the browser is just issuing the POST
.
I'd like to add that requests to the REST API are successful, thus the CORS configuration there must be right; although it looks to me that the http inbound adapter has the same configuration as those @RestController
s, something must be missing.
UPDATE
I've added another endpoint to specifically handle the OPTIONS
request:
</int-http:inbound-channel-adapter>
<int-http:inbound-channel-adapter id="httpInboundAdapterOptions"
channel="nullChannel"
path="/services/requests"
supported-methods="OPTIONS">
<int-http:cross-origin allow-credentials="false" allowed-headers="*" origin="*" />
</int-http:inbound-channel-adapter>
Now both the OPTIONS
and POST
requests are received and the latter starts the whole integration data processing; the browser still complains about a missing “Access-Control-Allow-Origin”
header though, and from the browser console I can see only there headers:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Length: 0
Date: Tue, 08 May 2018 14:29:30 GMT
By contrast, a GET
over a REST endpoint brings these:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Vary: Origin
Access-Control-Allow-Origin: *
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 08 May 2018 14:29:08 GMT