spring-cloud/spring-cloud-function

Content-Type cannot be set using message headers in GCP adapter

kimberninger opened this issue · 1 comments

Describe the bug

When using spring-cloud-function-adapter-gcp with a function that responds with a Message object to an HTTP trigger, the response's Content-Type header is not set according to the Message's corresponding header as presumably intended. It is rather overridden by the original request's content type.

It seems like PR #719 was originally meant to fix that the content type was incorrectly referenced by key contentType rather than the correct Content-Type, but actually it did not resolve that issue. Instead, it introduced the aforementioned bug. So now, the response contains two headers contentType and Content-Type which both do not reflect the value passed as message header.

Versions used:

  • Java 21
  • Spring Boot 3.3.2
  • Spring Cloud 2023.0.3
  • Spring Cloud Function & Spring Cloud Function Adapter GCP 4.1.3
  • Google Function Maven Plugin 0.11.0

Steps to reproduce

To reproduce the bug in a minimal setting, I used spring initializr with these settings to create a blank Spring Boot Application and replaced the contents of src/main/java/com/example/demo/DemoApplication.java with the following code:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;

import java.util.Map;
import java.util.function.Function;

@SpringBootApplication
public class DemoApplication {
    @Bean
    public Function<String, Message<?>> computation() {
        return request -> {
            var headers = new MessageHeaders(Map.of("Content-Type", "application/problem+json"));
            return MessageBuilder.createMessage(request, headers);
        };
    }

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Also, the Maven manifest had to be extended to include the Spring Cloud Function and Spring Cloud Function GCP Adapter, as well as Google's Function Maven plugin to run the function:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>21</java.version>
        <spring-cloud.version>2023.0.3</spring-cloud.version>
        <spring-cloud-function.version>4.1.3</spring-cloud-function.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-adapter-gcp</artifactId>
            <version>${spring-cloud-function.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <outputDirectory>target/deploy</outputDirectory>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-function-adapter-gcp</artifactId>
                        <version>${spring-cloud-function.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>com.google.cloud.functions</groupId>
                <artifactId>function-maven-plugin</artifactId>
                <version>0.11.0</version>
                <configuration>
                    <functionTarget>org.springframework.cloud.function.adapter.gcp.GcfJarLauncher</functionTarget>
                    <port>8080</port>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Finally, the Function Maven Plugin need a pointer to the application's main class in src/main/resources/META-INF/MANIFEST.MF:

Main-Class: com.example.demo.DemoApplication

After starting the application using ./mvnw function:run the function is now ready to accept requests:

curl -v localhost:8080 -H "Content-Type: application/json" -d '"Hello World"'

Expected behavior

The Content-Type header of the resulting response should have the value application/problem+json and no header named contentType should exist.

Actual behavior

The request above results in the following response:

HTTP/1.1 200 OK
Date: Thu, 08 Aug 2024 09:16:37 GMT
Content-Type: application/json
id: 6dcf1611-85f9-ae1d-ebd6-9a55539ac228
contentType: application/json
timestamp: 1723108597817
Transfer-Encoding: chunked
Server: Jetty(9.4.51.v20230217)

Both headers Content-Type and contentType exist and they both carry the unexpected value application/json.

If no content type is given with the request (curl -v localhost:8080 -d '"Hello World"') then the response looks as follows:

HTTP/1.1 200 OK
Date: Thu, 08 Aug 2024 09:14:18 GMT
Content-Type: application/x-www-form-urlencoded
id: 82984af0-d041-d585-2e08-f4c687009cee
contentType: application/json
timestamp: 1723108458897
Transfer-Encoding: chunked
Server: Jetty(9.4.51.v20230217)

Here the headers Content-Type and contentType still exist simultaneously, and they even carry different values.

It is probably also worth mentioning that the current behavior differs from the default behavior when using spring-cloud-starter-function-web instead of the GCP adapter to run the function. In this case, only Content-Type: application/problem+json results from the example above.