Http inbound adapter gives empty response on OPTIO

2019-07-26 01:09发布

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)

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:

  1. a table that pulls data from the REST API
    • this part works, the frontend successfully retrieves data from the backend
  2. 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 @RestControllers, 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

0条回答
登录 后发表回答