Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RoutingFunction fails with ArrayList to String ClassCastException #1106

Closed
dbondarecych opened this issue Jan 22, 2024 · 7 comments
Closed

Comments

@dbondarecych
Copy link

dbondarecych commented Jan 22, 2024

Describe the bug
Default RoutingFunction fails with:
java.lang.ClassCastException: class java.util.ArrayList cannot be cast to class java.lang.String (java.util.ArrayList and java.lang.String are in module java.base of loader 'bootstrap')
at org.springframework.cloud.function.context.config.RoutingFunction.route(RoutingFunction.java:136)
at org.springframework.cloud.function.context.config.RoutingFunction.apply(RoutingFunction.java:107)

The root cause: message.getHeaders().get(FunctionProperties.FUNCTION_DEFINITION) - returns ArrayList, which is casted to String.

Steps to reproduce
Specify request/message header: spring.cloud.function.definition=uppercase

curl http://localhost:8080
-H "Content-Type: text/plain"
-H "func_name: uppercase"
--data-urlencode "some Txt"

Sample

@SpringBootApplication
public class CloudFunctionMain {

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

    @Bean
    public Function<String, String> uppercase() {
        return value -> value.toUpperCase();
    }

    @Bean
    public Function<String, String> reverse() {
        return s -> new StringBuilder(s).reverse().toString();
    }
    
}
@olegz
Copy link
Contributor

olegz commented Mar 27, 2024

Unfortunately I can not reproduce it. I just tested it with

curl http://localhost:8080/functionRouter -H "Content-Type: text/plain" -H "func_name: uppercase" -H "spring.cloud.function.definition: uppercase" --data-urlencode "some Txt"

and everything works as excepted.

Please provide a reproducible sample as I can not see how headers for function definition could be given as list

@TamasNeumer
Copy link

TamasNeumer commented Apr 12, 2024

Hi @olegz,

I have stumbled upon the same issue.
I was able to reporiduce it and uploaded the codebase here:

https://github.com/TamasNeumer/gcf-spring-debug/tree/main/src

curl --location --request GET 'http://localhost:8080/' \
--header 'spring.cloud.function.definition: uppercase' \
--header 'Content-Type: text/plain'

BR,
Tamas

@TamasNeumer
Copy link

Hi @olegz

could you please give some feedback on this?

Thank you!

@olegz
Copy link
Contributor

olegz commented Apr 26, 2024

I tried to run your sample but it doesn't start. Is it GCF specific?

@TamasNeumer
Copy link

Hi @olegz,

Forgot to mention that you need to use the following Maven command to start:

mvn function:run -Drun.funtionTarget=com.tamasne.cloudfunctiondebug.CloudFunctionDebugApplication -Drun.port=8080

@akenra
Copy link
Contributor

akenra commented May 11, 2024

Hey folks, this is occuring basically because of how com.google.cloud.functions.HttpMessage#getHeaders() API is designed. if we refer to the source code:
https://github.com/GoogleCloudPlatform/functions-framework-java/blob/main/functions-framework-api/src/main/java/com/google/cloud/functions/HttpMessage.java#L89

We can see in the javadoc the following:

 /**
   * Returns a map describing the headers of this HTTP request, or this part of a multipart request.
   * If the headers look like this...
   *
   * <pre>
   *   Content-Type: text/plain
   *   Some-Header: some value
   *   Some-Header: another value
   * </pre>
   *
   * ...then the returned value will map {@code "Content-Type"} to a one-element list containing
   * {@code "text/plain"}, and {@code "Some-Header"} to a two-element list containing {@code "some
   * value"} and {@code "another value"}.
   *
   * @return a map where each key is an HTTP header and the corresponding {@code List} value has one
   *     element for each occurrence of that header.
   */

So, when a new instance of org.springframework.messaging.Message is created in

the headers are copied as-is from the incoming httpRequest (which is an instance of a subclass of an com.google.cloud.functions.HttpMessage).

Then, in the org.springframework.cloud.function.context.config.RoutingFunction#locateFunctionFromDefinitionOrExpression() we attempt to cast this one-element list to a String which obviously fails.
In the debugger you can see the structure of the headers pretty clearly:
header_value_ArrayList

@akenra
Copy link
Contributor

akenra commented May 11, 2024

As for the solution to the aforementioned problem, I'd like to share some of my findings after a bit of digging:

  1. https://www.rfc-editor.org/rfc/rfc9110#name-field-order explicitly states that:
    "A recipient MAY combine multiple field lines within a field section that have the same field name into one field line, without changing the semantics of the message, by appending each subsequent field line value to the initial field line value in order, separated by a comma (",") and optional whitespace (OWS, defined in Section 5.6.3). For consistency, use comma SP."

So we're safe to combine values here in terms of HTTP semantics (not that the RFC matters that much here I guess).

  1. As per documentation at https://docs.spring.io/spring-cloud-function/docs/current/reference/html/spring-cloud-function.html#_declarative_function_composition and https://docs.spring.io/spring-cloud-function/docs/current/reference/html/spring-cloud-function.html#_messageroutingcallback, the composition of functions with "|" and "," separators is allowed in this "spring.cloud.function.definition" header.

Given my findings, I came to a conclustion that it seems to be possible to add a conditional to org.springframework.cloud.function.context.config.RoutingFunction#locateFunctionFromDefinitionOrExpression() that:
a) Checks if the header entry's value is a N-value list;
b) Combines in the incoming order the N values from a list into a single string separated by valid function composition syntax separators.
Which, in turn, judging by the implementations of org.springframework.cloud.function.context.FunctionCatalog#lookup(), will be correctly resolved further down the call stack.

Here's a code example to further illustrate my point:

	private FunctionInvocationWrapper locateFunctionFromDefinitionOrExpression(Message<?> message) {
		for (Entry<String, Object> headerEntry : message.getHeaders().entrySet()) {
			String headerKey = headerEntry.getKey();
			Object headerValue = headerEntry.getValue();

			if (headerKey == null || headerValue == null) {
				continue;
			}

			boolean isFunctionDefinition = FunctionProperties.FUNCTION_DEFINITION.equalsIgnoreCase(headerKey);
			boolean isRoutingExpression = FunctionProperties.ROUTING_EXPRESSION.equalsIgnoreCase(headerKey);

			if (isFunctionDefinition) {
				if (headerValue instanceof String definition) {
					return functionFromDefinition(definition);
				}
				else if (headerValue instanceof List<?> definitions && !definitions.isEmpty()) {
					return functionFromDefinition(definitions.stream().map(Object::toString).collect(Collectors.joining(",")));
				}
			}
			else if (isRoutingExpression) {
				if (headerValue instanceof String expression) {
					return functionFromExpression(expression, message, true);
				}
				else if (headerValue instanceof List<?> expressions && !expressions.isEmpty()) {
					return functionFromExpression(expressions.get(0).toString(), message, true);
				}
			}
		}
		return null;
	}

The "spring.cloud.function.routing-expression" will certainly require some special treatment as well, but the same solution as for the header "spring.cloud.function.definition" won't work for it as it's probably nonsensical to combine two (or more) values from duplicate "spring.cloud.function.routing-expression" headers. In this case it's probably fine to simply take the first value from a list and document this behavior as such?

So there's that, I guess, @olegz please correct me if I'm wrong.

akenra added a commit to akenra/spring-cloud-function that referenced this issue May 12, 2024
…ion.definition" header contains a List value instead of a String value (GCP-specific)

Resolves spring-cloud#1106
@olegz olegz closed this as completed in faeb0f7 Jun 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants