Remove spring-cloud-function-adapter-aws-web, fix Proxy response to return empty collection if header is not present

This commit is contained in:
Oleg Zhurakousky
2023-07-10 15:56:01 +02:00
parent a744e7af1a
commit 21cc097ec1
23 changed files with 4 additions and 1474 deletions

View File

@@ -23,7 +23,6 @@
<module>spring-cloud-function-grpc</module>
<module>spring-cloud-function-grpc-cloudevent-ext</module>
<module>spring-cloud-function-serverless-web</module>
<module>spring-cloud-function-adapter-aws-web</module>
<module>spring-cloud-function-adapter-azure-web</module>
</modules>

View File

@@ -1,20 +0,0 @@
#### Introduction
This module represents a concept of a light weight AWS forwarding proxy which deploys and interacts with existing
Spring Boot web application deployed as AWS Lambda.
A sample is provided in `/sample` directory
=======
Classes in this package should ideally reside in spring-web somewhere as a light weight HTTP proxy, since they are independent of the
context of the execution (i.e., AWS or Azure or whatever).
In fact classes in these package is a slimed-down copy of similar classes in MockMVC.
A sample is provided in `/sample` directory
A sample is provided in [sample](https://github.com/spring-cloud/spring-cloud-function/tree/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store) directory. It contain README and SAM template file to simplify the deployment. This module is identified as the only additional dependnecy to the existing web-app.
_NOTE: Although this module is AWS specific, this dependency is protocol only (not binary), therefore there is no AWS dependnecies._
The aformentioned proxy is identified as AWS Lambda [handler](https://github.com/spring-cloud/spring-cloud-function/blob/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml#L14)
The main Spring Boot configuration file is identified as [MAIN_CLASS](https://github.com/spring-cloud/spring-cloud-function/blob/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml#L22)

View File

@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
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>
<artifactId>spring-cloud-function-adapter-aws-web</artifactId>
<packaging>jar</packaging>
<name>spring-cloud-function-adapter-aws-web</name>
<description>AWS Lambda Adapter for Spring Cloud Function</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-serverless-web</artifactId>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,12 +0,0 @@
# This file is auto generated by SAM CLI build command
[function_build_definitions.4c7d6b2d-fe8c-493a-945e-f0eaf34aaf65]
codeuri = "/Users/ozhurakousky/Documents/dev/repo/spring-cloud-function/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store"
runtime = "java17"
architecture = "x86_64"
handler = "org.springframework.cloud.function.adapter.aws.web.WebProxyInvoker::handleRequest"
manifest_hash = ""
packagetype = "Zip"
functions = ["PetStoreFunction"]
[layer_build_definitions]

View File

@@ -1,39 +0,0 @@
Modeled after https://github.com/awslabs/aws-serverless-java-container/tree/main/samples/spring/pet-store
# Serverless Spring example
A basic pet store written with the [Spring framework](https://projects.spring.io/spring-framework/).
The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition.
## Pre-requisites
* [AWS CLI](https://aws.amazon.com/cli/)
* [SAM CLI](https://github.com/awslabs/aws-sam-cli)
* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/)
## Deployment
In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package
```
$ sam build
```
This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory.
To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen
```
$ sam deploy --guided
```
Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL
```
...
---------------------------------------------------------------------------------------------------------
OutputKey-Description OutputValue
---------------------------------------------------------------------------------------------------------
PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets
---------------------------------------------------------------------------------------------------------
$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets
```

View File

@@ -1,161 +0,0 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>oz.spring.petstore</groupId>
<artifactId>pet-store</artifactId>
<version>1.0-SNAPSHOT</version><!-- @releaser:version-check-off -->
<name>pet-store</name>
<description>Simple pet store written with the Spring framework</description>
<url>https://aws.amazon.com/lambda/</url>
<scm>
<url>https://github.com/awslabs/aws-serverless-java-container.git</url>
</scm>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<spring.version>6.0.8</spring.version>
<junit.version>4.13.2</junit.version>
<log4j.version>2.19.0</log4j.version>
</properties>
<dependencies>
<dependency>
<groupId>io.github.crac</groupId>
<artifactId>org-crac</artifactId>
<version>0.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws-web</artifactId>
<version>4.0.3-SNAPSHOT</version><!-- @releaser:version-check-off -->
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>shaded-jar</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer
implementation="com.github.edwgiz.mavenShadePlugin.log4j2CacheTransformer.PluginsCacheFileTransformer">
</transformer>
</transformers>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.github.edwgiz</groupId>
<artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId>
<version>2.8.1</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>assembly-zip</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<!-- don't build a jar, we'll use the classes dir -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>default-jar</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.0.0-M1</version><!-- @releaser:version-check-off -->
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<!-- select and copy only runtime dependencies to a temporary lib folder -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>zip-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>${project.artifactId}-${project.version}</finalName>
<descriptors>
<descriptor>src${file.separator}assembly${file.separator}bin.xml</descriptor>
</descriptors>
<attach>false</attach>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -1,24 +0,0 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>lambda-package</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<!-- copy runtime dependencies with some exclusions -->
<fileSet>
<directory>${project.build.directory}${file.separator}lib</directory>
<outputDirectory>lib</outputDirectory>
</fileSet>
<!-- copy all classes -->
<fileSet>
<directory>${project.build.directory}${file.separator}classes</directory>
<includes>
<include>**</include>
</includes>
<outputDirectory>${file.separator}</outputDirectory>
</fileSet>
</fileSets>
</assembly>

View File

@@ -1,56 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package oz.spring.petstore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Configuration
@Import({ PetsController.class })
public class PetStoreSpringAppConfig {
/*
* optimization - avoids creating default exception resolvers; not required as the serverless container handles
* all exceptions
*
* By default, an ExceptionHandlerExceptionResolver is created which creates many dependent object, including
* an expensive ObjectMapper instance.
*
* To enable custom @ControllerAdvice classes remove this bean.
*/
@Bean
public HandlerExceptionResolver handlerExceptionResolver() {
return new HandlerExceptionResolver() {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
return null;
}
};
}
}

View File

@@ -1,80 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package oz.spring.petstore;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import oz.spring.petstore.model.Pet;
import oz.spring.petstore.model.PetData;
import java.security.Principal;
import java.util.Optional;
import java.util.UUID;
@RestController
@EnableWebMvc
public class PetsController {
@RequestMapping(path = "/pets", method = RequestMethod.POST)
public Pet createPet(@RequestBody Pet newPet) {
if (newPet.getName() == null || newPet.getBreed() == null) {
return null;
}
Pet dbPet = newPet;
dbPet.setId(UUID.randomUUID().toString());
return dbPet;
}
@RequestMapping(path = "/pets", method = RequestMethod.GET)
public Pet[] listPets(@RequestParam("limit") Optional<Integer> limit, Principal principal) {
int queryLimit = 10;
if (limit.isPresent()) {
queryLimit = limit.get();
}
Pet[] outputPets = new Pet[queryLimit];
for (int i = 0; i < queryLimit; i++) {
Pet newPet = new Pet();
newPet.setId(UUID.randomUUID().toString());
newPet.setName(PetData.getRandomName());
newPet.setBreed(PetData.getRandomBreed());
newPet.setDateOfBirth(PetData.getRandomDoB());
outputPets[i] = newPet;
}
return outputPets;
}
@GetMapping("favicon.ico")
@ResponseBody
void returnNoFavicon() {
}
@RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET)
public Pet listPets() {
Pet newPet = new Pet();
newPet.setId(UUID.randomUUID().toString());
newPet.setBreed(PetData.getRandomBreed());
newPet.setDateOfBirth(PetData.getRandomDoB());
newPet.setName(PetData.getRandomName());
return newPet;
}
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package oz.spring.petstore.model;
public class Error {
private String message;
public Error(String errorMessage) {
message = errorMessage;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -1,58 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package oz.spring.petstore.model;
import java.util.Date;
public class Pet {
private String id;
private String breed;
private String name;
private Date dateOfBirth;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getBreed() {
return breed;
}
public void setBreed(String breed) {
this.breed = breed;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getDateOfBirth() {
return dateOfBirth;
}
public void setDateOfBirth(Date dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
}

View File

@@ -1,115 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package oz.spring.petstore.model;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
public class PetData {
private static List<String> breeds = new ArrayList<>();
static {
breeds.add("Afghan Hound");
breeds.add("Beagle");
breeds.add("Bernese Mountain Dog");
breeds.add("Bloodhound");
breeds.add("Dalmatian");
breeds.add("Jack Russell Terrier");
breeds.add("Norwegian Elkhound");
}
private static List<String> names = new ArrayList<>();
static {
names.add("Bailey");
names.add("Bella");
names.add("Max");
names.add("Lucy");
names.add("Charlie");
names.add("Molly");
names.add("Buddy");
names.add("Daisy");
names.add("Rocky");
names.add("Maggie");
names.add("Jake");
names.add("Sophie");
names.add("Jack");
names.add("Sadie");
names.add("Toby");
names.add("Chloe");
names.add("Cody");
names.add("Bailey");
names.add("Buster");
names.add("Lola");
names.add("Duke");
names.add("Zoe");
names.add("Cooper");
names.add("Abby");
names.add("Riley");
names.add("Ginger");
names.add("Harley");
names.add("Roxy");
names.add("Bear");
names.add("Gracie");
names.add("Tucker");
names.add("Coco");
names.add("Murphy");
names.add("Sasha");
names.add("Lucky");
names.add("Lily");
names.add("Oliver");
names.add("Angel");
names.add("Sam");
names.add("Princess");
names.add("Oscar");
names.add("Emma");
names.add("Teddy");
names.add("Annie");
names.add("Winston");
names.add("Rosie");
}
public static List<String> getBreeds() {
return breeds;
}
public static List<String> getNames() {
return names;
}
public static String getRandomBreed() {
return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1));
}
public static String getRandomName() {
return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1));
}
public static Date getRandomDoB() {
GregorianCalendar gc = new GregorianCalendar();
int year = ThreadLocalRandom.current().nextInt(
Calendar.getInstance().get(Calendar.YEAR) - 15,
Calendar.getInstance().get(Calendar.YEAR)
);
gc.set(Calendar.YEAR, year);
int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR));
gc.set(Calendar.DAY_OF_YEAR, dayOfYear);
return gc.getTime();
}
}

View File

@@ -1,41 +0,0 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Example Pet Store API written with spring-cloud-function web-proxy support
Globals:
Api:
# API Gateway regional endpoints
EndpointConfiguration: REGIONAL
Resources:
PetStoreFunction:
Type: AWS::Serverless::Function
Properties:
# AutoPublishAlias: bcn
FunctionName: pet-store-17
Handler: org.springframework.cloud.function.adapter.aws.web.WebProxyInvoker::handleRequest
Runtime: java17
SnapStart:
ApplyOn: PublishedVersions
CodeUri: .
MemorySize: 1024
Policies: AWSLambdaBasicExecutionRole
Timeout: 30
Environment:
Variables:
MAIN_CLASS: oz.spring.petstore.PetStoreSpringAppConfig
Events:
HttpApiEvent:
Type: HttpApi
Properties:
TimeoutInMillis: 20000
PayloadFormatVersion: '1.0'
Outputs:
SpringPetStoreApi:
Description: URL for application
Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets'
Export:
Name: SpringPetStoreApi

View File

@@ -1,140 +0,0 @@
/*
* Copyright 2019-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.adapter.aws.web;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
//import org.springframework.boot.SpringBootConfiguration;
//import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.KotlinDetector;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* General utility class which aggregates various class-level utility functions
* used by the framework.
*
* @author Oleg Zhurakousky
* @since 3.0.1
*/
public final class FunctionClassUtils {
private static Log logger = LogFactory.getLog(FunctionClassUtils.class);
private FunctionClassUtils() {
}
/**
* Discovers the start class in the currently running application.
* The discover search order is 'MAIN_CLASS' environment property,
* 'MAIN_CLASS' system property, META-INF/MANIFEST.MF:'Start-Class' attribute,
* meta-inf/manifest.mf:'Start-Class' attribute.
*
* @return instance of Class which represent the start class of the application.
*/
public static Class<?> getStartClass() {
ClassLoader classLoader = FunctionClassUtils.class.getClassLoader();
return getStartClass(classLoader);
}
static Class<?> getStartClass(ClassLoader classLoader) {
Class<?> mainClass = null;
if (System.getenv("MAIN_CLASS") != null) {
mainClass = ClassUtils.resolveClassName(System.getenv("MAIN_CLASS"), classLoader);
}
else if (System.getProperty("MAIN_CLASS") != null) {
mainClass = ClassUtils.resolveClassName(System.getProperty("MAIN_CLASS"), classLoader);
}
else {
try {
Class<?> result = getStartClass(
Collections.list(classLoader.getResources(JarFile.MANIFEST_NAME)), classLoader);
if (result == null) {
result = getStartClass(Collections
.list(classLoader.getResources("meta-inf/manifest.mf")), classLoader);
}
Assert.notNull(result, "Failed to locate main class");
mainClass = result;
}
catch (Exception ex) {
throw new IllegalStateException("Failed to discover main class. An attempt was made to discover "
+ "main class as 'MAIN_CLASS' environment variable, system property as well as "
+ "entry in META-INF/MANIFEST.MF (in that order).", ex);
}
}
logger.info("Main class: " + mainClass);
return mainClass;
}
private static Class<?> getStartClass(List<URL> list, ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Searching manifests: " + list);
}
for (URL url : list) {
try {
InputStream inputStream = null;
Manifest manifest = new Manifest(url.openStream());
logger.info("Searching for start class in manifest: " + url);
if (logger.isDebugEnabled()) {
manifest.write(System.out);
}
try {
String startClassName = manifest.getMainAttributes().getValue("Start-Class");
if (!StringUtils.hasText(startClassName)) {
startClassName = manifest.getMainAttributes().getValue("Main-Class");
}
if (StringUtils.hasText(startClassName)) {
Class<?> startClass = ClassUtils.forName(startClassName, classLoader);
if (KotlinDetector.isKotlinType(startClass)) {
PathMatchingResourcePatternResolver r = new PathMatchingResourcePatternResolver(classLoader);
String packageName = startClass.getPackage().getName();
Resource[] resources = r.getResources("classpath:" + packageName.replace(".", "/") + "/*.class");
for (int i = 0; i < resources.length; i++) {
Resource resource = resources[i];
String className = packageName + "." + (resource.getFilename().replace("/", ".")).replace(".class", "");
startClass = ClassUtils.forName(className, classLoader);
}
}
}
}
finally {
if (inputStream != null) {
inputStream.close();
}
}
}
catch (Exception ex) {
logger.debug("Failed to determine Start-Class in manifest file of " + url, ex);
}
}
return null;
}
}

View File

@@ -1,5 +0,0 @@
Classes in this package would remain specific to AWS (in this case). There would be something similar in Azure and others.
And these classes would depend on what is currently in `org.springframework.web.client` package of this module.
However, ideally the contents of the `org.springframework.web.client` package should reside in spring-web somewhere as a light weight
HTTP proxy as we technically already have it in a form of MockMVC.

View File

@@ -1,146 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.adapter.aws.web;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.function.serverless.web.ProxyHttpServletRequest;
import org.springframework.cloud.function.serverless.web.ProxyHttpServletResponse;
import org.springframework.cloud.function.serverless.web.ProxyMvc;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
*
* AWS Lambda specific handler that will proxy API Gateway request to Spring Web-app
* This class represents AWS Lambda fronted by API Gateway and is identified as 'handler' during the deployment.
*
* @author Oleg Zhurakousky
*
*/
public class WebProxyInvoker {
private static Log logger = LogFactory.getLog(WebProxyInvoker.class);
private final ProxyMvc mvc;
ObjectMapper mapper = new ObjectMapper();
public WebProxyInvoker() throws ServletException {
Class<?> startClass = FunctionClassUtils.getStartClass();
this.mvc = ProxyMvc.INSTANCE(startClass);
}
/*
* TODO
* - Security context propagation from AWS API Gateway (easy)
* - Error handling
*/
@SuppressWarnings("unchecked")
private HttpServletRequest prepareRequest(InputStream input) throws IOException {
Map<String, Object> request = mapper.readValue(input, Map.class);
if (logger.isInfoEnabled()) {
logger.info("Request: " + request);
}
String httpMethod = (String) request.get("httpMethod");
String path = (String) request.get("path");
if (logger.isDebugEnabled()) {
logger.debug("httpMethod: " + httpMethod);
logger.debug("path: " + path);
}
ProxyHttpServletRequest httpRequest = new ProxyHttpServletRequest(this.mvc.getServletContext(), httpMethod, path);
// CONTENT
if (StringUtils.hasText((String) request.get("body"))) {
httpRequest.setContent(((String) request.get("body")).getBytes());
}
// REQUEST PARAM
if (request.get("multiValueQueryStringParameters") != null) {
Map<String, List<String>> parameters = (Map<String, List<String>>) request.get("multiValueQueryStringParameters");
for (Entry<String, List<String>> parameter : parameters.entrySet()) {
httpRequest.setParameter(parameter.getKey(), parameter.getValue().toArray(new String[] {}));
}
}
// HEADERS
Map<String, List<String>> headers = (Map<String, List<String>>) request.get("multiValueHeaders");
HttpHeaders httpHeaders = new HttpHeaders();
for (Entry<String, List<String>> entry : headers.entrySet()) {
// TODO may need to do some header formatting
httpHeaders.addAll(entry.getKey(), entry.getValue());
}
httpRequest.setHeaders(httpHeaders);
return httpRequest;
}
public void handleRequest(InputStream input, OutputStream output) throws IOException {
HttpServletRequest httpRequest = this.prepareRequest(input);
ProxyHttpServletResponse httpResponse = new ProxyHttpServletResponse();
try {
this.mvc.service(httpRequest, httpResponse);
}
catch (Exception e) {
e.printStackTrace();
throw new IllegalStateException(e);
}
String responseString = httpResponse.getContentAsString();
if (StringUtils.hasText(responseString)) {
if (logger.isDebugEnabled()) {
logger.debug("Response: " + responseString);
}
Map<String, Object> apiGatewayResponseStructure = new HashMap<String, Object>();
apiGatewayResponseStructure.put("isBase64Encoded", false);
apiGatewayResponseStructure.put("statusCode", HttpStatus.OK.value());
apiGatewayResponseStructure.put("body", responseString);
Map<String, List<String>> multiValueHeaders = new HashMap<>();
Map<String, String> headers = new HashMap<>();
for (String headerName : httpResponse.getHeaderNames()) {
multiValueHeaders.put(headerName, httpResponse.getHeaders(headerName));
headers.put(headerName, httpResponse.getHeaders(headerName).toString());
}
headers.put(HttpHeaders.CONTENT_TYPE, httpResponse.getContentType());
multiValueHeaders.put(HttpHeaders.CONTENT_TYPE, Collections.singletonList(httpResponse.getContentType()));
apiGatewayResponseStructure.put("multiValueHeaders", multiValueHeaders);
apiGatewayResponseStructure.put("headers", headers);
byte[] apiGatewayResponseBytes = mapper.writeValueAsBytes(apiGatewayResponseStructure);
StreamUtils.copy(apiGatewayResponseBytes, output);
}
}
}

View File

@@ -1,58 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.adapter.aws.web;
import java.util.Date;
public class Pet {
private String id;
private String breed;
private String name;
private Date dateOfBirth;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getBreed() {
return breed;
}
public void setBreed(String breed) {
this.breed = breed;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getDateOfBirth() {
return dateOfBirth;
}
public void setDateOfBirth(Date dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
}

View File

@@ -1,118 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.adapter.aws.web;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class PetData {
private static List<String> breeds = new ArrayList<>();
static {
breeds.add("Afghan Hound");
breeds.add("Beagle");
breeds.add("Bernese Mountain Dog");
breeds.add("Bloodhound");
breeds.add("Dalmatian");
breeds.add("Jack Russell Terrier");
breeds.add("Norwegian Elkhound");
}
private static List<String> names = new ArrayList<>();
static {
names.add("Bailey");
names.add("Bella");
names.add("Max");
names.add("Lucy");
names.add("Charlie");
names.add("Molly");
names.add("Buddy");
names.add("Daisy");
names.add("Rocky");
names.add("Maggie");
names.add("Jake");
names.add("Sophie");
names.add("Jack");
names.add("Sadie");
names.add("Toby");
names.add("Chloe");
names.add("Cody");
names.add("Bailey");
names.add("Buster");
names.add("Lola");
names.add("Duke");
names.add("Zoe");
names.add("Cooper");
names.add("Abby");
names.add("Riley");
names.add("Ginger");
names.add("Harley");
names.add("Roxy");
names.add("Bear");
names.add("Gracie");
names.add("Tucker");
names.add("Coco");
names.add("Murphy");
names.add("Sasha");
names.add("Lucky");
names.add("Lily");
names.add("Oliver");
names.add("Angel");
names.add("Sam");
names.add("Princess");
names.add("Oscar");
names.add("Emma");
names.add("Teddy");
names.add("Annie");
names.add("Winston");
names.add("Rosie");
}
public static List<String> getBreeds() {
return breeds;
}
public static List<String> getNames() {
return names;
}
public static String getRandomBreed() {
return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1));
}
public static String getRandomName() {
return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1));
}
public static Date getRandomDoB() {
GregorianCalendar gc = new GregorianCalendar();
int year = ThreadLocalRandom.current().nextInt(Calendar.getInstance().get(Calendar.YEAR) - 15,
Calendar.getInstance().get(Calendar.YEAR));
gc.set(Calendar.YEAR, year);
int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR));
gc.set(Calendar.DAY_OF_YEAR, dayOfYear);
return gc.getTime();
}
}

View File

@@ -1,69 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.adapter.aws.web;
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
//@SpringBootApplication
@Configuration
@Import({ PetsController.class })
public class PetStoreSpringAppConfig {
/*
* Create required HandlerMapping, to avoid several default HandlerMapping
* instances being created
*/
@Bean
public HandlerMapping handlerMapping() {
return new RequestMappingHandlerMapping();
}
/*
* Create required HandlerAdapter, to avoid several default HandlerAdapter
* instances being created
*/
@Bean
public HandlerAdapter handlerAdapter() {
return new RequestMappingHandlerAdapter();
}
@Bean
public Filter filter() {
return new Filter() {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("FILTER ===> Hello from: " + request.getLocalAddr());
chain.doFilter(request, response);
}
};
}
}

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.adapter.aws.web;
import java.security.Principal;
import java.util.Optional;
import java.util.UUID;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@RestController
@EnableWebMvc
public class PetsController {
@RequestMapping(path = "/pets", method = RequestMethod.POST)
public Pet createPet(@RequestBody Pet newPet) {
if (newPet.getName() == null || newPet.getBreed() == null) {
return null;
}
Pet dbPet = newPet;
dbPet.setId(UUID.randomUUID().toString());
return dbPet;
}
@RequestMapping(path = "/pets", method = RequestMethod.GET)
public Pet[] listPets(@RequestParam("limit") Optional<Integer> limit, Principal principal) {
System.out.println("=====> EXECUTING");
int queryLimit = 10;
if (limit.isPresent()) {
queryLimit = limit.get();
}
Pet[] outputPets = new Pet[queryLimit];
for (int i = 0; i < queryLimit; i++) {
Pet newPet = new Pet();
newPet.setId(UUID.randomUUID().toString());
newPet.setName(PetData.getRandomName());
newPet.setBreed(PetData.getRandomBreed());
newPet.setDateOfBirth(PetData.getRandomDoB());
outputPets[i] = newPet;
}
return outputPets;
}
@RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET)
public Pet listPets() {
System.out.println("=====> Getting pet by id");
Pet newPet = new Pet();
newPet.setId(UUID.randomUUID().toString());
newPet.setBreed(PetData.getRandomBreed());
newPet.setDateOfBirth(PetData.getRandomDoB());
newPet.setName(PetData.getRandomName());
return newPet;
}
}

View File

@@ -1,175 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.adapter.aws.web;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.function.serverless.web.ProxyHttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Oleg Zhurakousky
*/
public class WebProxyInvokerTests {
static String API_GATEWAY_EVENT = "{\n"
+ " \"version\": \"1.0\",\n"
+ " \"resource\": \"$default\",\n"
+ " \"path\": \"/pets\",\n"
+ " \"httpMethod\": \"POST\",\n"
+ " \"headers\": {\n"
+ " \"Content-Length\": \"45\",\n"
+ " \"Content-Type\": \"application/json\",\n"
+ " \"Host\": \"i76bfhczs0.execute-api.eu-west-3.amazonaws.com\",\n"
+ " \"User-Agent\": \"curl/7.79.1\",\n"
+ " \"X-Amzn-Trace-Id\": \"Root=1-64087690-2151375b219d3ba3389ea84e\",\n"
+ " \"X-Forwarded-For\": \"109.210.252.44\",\n"
+ " \"X-Forwarded-Port\": \"443\",\n"
+ " \"X-Forwarded-Proto\": \"https\",\n"
+ " \"accept\": \"*/*\"\n"
+ " },\n"
+ " \"multiValueHeaders\": {\n"
+ " \"Content-Length\": [\n"
+ " \"45\"\n"
+ " ],\n"
+ " \"Content-Type\": [\n"
+ " \"application/json\"\n"
+ " ],\n"
+ " \"Host\": [\n"
+ " \"i76bfhczs0.execute-api.eu-west-3.amazonaws.com\"\n"
+ " ],\n"
+ " \"User-Agent\": [\n"
+ " \"curl/7.79.1\"\n"
+ " ],\n"
+ " \"X-Amzn-Trace-Id\": [\n"
+ " \"Root=1-64087690-2151375b219d3ba3389ea84e\"\n"
+ " ],\n"
+ " \"X-Forwarded-For\": [\n"
+ " \"109.210.252.44\"\n"
+ " ],\n"
+ " \"X-Forwarded-Port\": [\n"
+ " \"443\"\n"
+ " ],\n"
+ " \"X-Forwarded-Proto\": [\n"
+ " \"https\"\n"
+ " ],\n"
+ " \"accept\": [\n"
+ " \"*/*\"\n"
+ " ]\n"
+ " },\n"
+ " \"queryStringParameters\": {\n"
+ " \"abc\": \"xyz\",\n"
+ " \"foo\": \"baz\"\n"
+ " },\n"
+ " \"multiValueQueryStringParameters\": {\n"
+ " \"abc\": [\n"
+ " \"xyz\"\n"
+ " ],\n"
+ " \"foo\": [\n"
+ " \"bar\",\n"
+ " \"baz\"\n"
+ " ]\n"
+ " },\n"
+ " \"requestContext\": {\n"
+ " \"accountId\": \"123456789098\",\n"
+ " \"apiId\": \"i76bfhczs0\",\n"
+ " \"domainName\": \"i76bfhczs0.execute-api.eu-west-3.amazonaws.com\",\n"
+ " \"domainPrefix\": \"i76bfhczs0\",\n"
+ " \"extendedRequestId\": \"Bdd2ngt5iGYEMIg=\",\n"
+ " \"httpMethod\": \"POST\",\n"
+ " \"identity\": {\n"
+ " \"accessKey\": null,\n"
+ " \"accountId\": null,\n"
+ " \"caller\": null,\n"
+ " \"cognitoAmr\": null,\n"
+ " \"cognitoAuthenticationProvider\": null,\n"
+ " \"cognitoAuthenticationType\": null,\n"
+ " \"cognitoIdentityId\": null,\n"
+ " \"cognitoIdentityPoolId\": null,\n"
+ " \"principalOrgId\": null,\n"
+ " \"sourceIp\": \"109.210.252.44\",\n"
+ " \"user\": null,\n"
+ " \"userAgent\": \"curl/7.79.1\",\n"
+ " \"userArn\": null\n"
+ " },\n"
+ " \"path\": \"/pets\",\n"
+ " \"protocol\": \"HTTP/1.1\",\n"
+ " \"requestId\": \"Bdd2ngt5iGYEMIg=\",\n"
+ " \"requestTime\": \"08/Mar/2023:11:50:40 +0000\",\n"
+ " \"requestTimeEpoch\": 1678276240455,\n"
+ " \"resourceId\": \"$default\",\n"
+ " \"resourcePath\": \"$default\",\n"
+ " \"stage\": \"$default\"\n"
+ " },\n"
+ " \"pathParameters\": null,\n"
+ " \"stageVariables\": null,\n"
+ " \"body\": \"{\\\"id\\\":\\\"123\\\",\\\"breed\\\":\\\"Datsun\\\",\\\"name\\\":\\\"Donald\\\"}\",\n"
+ " \"isBase64Encoded\": false\n"
+ "}";
@Test
public void validateHttpServletRequestConstruction() throws Exception {
System.setProperty("MAIN_CLASS", PetStoreSpringAppConfig.class.getName());
WebProxyInvoker invoker = new WebProxyInvoker();
InputStream targetStream = new ByteArrayInputStream(API_GATEWAY_EVENT.getBytes());
Method prepareRequest = ReflectionUtils.findMethod(WebProxyInvoker.class, "prepareRequest", InputStream.class);
prepareRequest.setAccessible(true);
ProxyHttpServletRequest request = (ProxyHttpServletRequest) prepareRequest.invoke(invoker, targetStream);
assertThat(request.getContentType()).isEqualTo("application/json");
assertThat(request.getParameterValues("foo").length).isEqualTo(2);
assertThat(request.getParameterValues("foo")[0]).isEqualTo("bar");
assertThat(request.getParameterValues("abc").length).isEqualTo(1);
assertThat(request.getParameterValues("abc")[0]).isEqualTo("xyz");
assertThat(request.getHeaders(HttpHeaders.CONTENT_TYPE).nextElement()).isEqualTo("application/json");
assertThat(request.getContentAsString()).isEqualTo("{\"id\":\"123\",\"breed\":\"Datsun\",\"name\":\"Donald\"}");
assertThat(request.getContentLength()).isEqualTo(45);
assertThat(request.getMethod()).isEqualTo("POST");
}
@Test
public void testApiGatewayProxy() throws Exception {
System.setProperty("MAIN_CLASS", PetStoreSpringAppConfig.class.getName());
WebProxyInvoker invoker = new WebProxyInvoker();
InputStream targetStream = new ByteArrayInputStream(API_GATEWAY_EVENT.getBytes());
ByteArrayOutputStream output = new ByteArrayOutputStream();
invoker.handleRequest(targetStream, output);
ObjectMapper mapper = new ObjectMapper();
Map result = mapper.readValue(output.toByteArray(), Map.class);
assertThat((boolean) result.get("isBase64Encoded")).isFalse();
assertThat(((Map<String, List<String>>) result.get("multiValueHeaders")).get("Content-Type").get(0)).isEqualTo("application/json");
assertThat(result.get("statusCode")).isEqualTo(200);
Pet pet = mapper.readValue((String) result.get("body"), Pet.class);
assertThat(pet.getName()).isEqualTo("Donald");
}
}

View File

@@ -27,6 +27,7 @@ import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@@ -264,6 +265,9 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
*/
@Override
public List<String> getHeaders(String name) {
if (!this.headers.containsKey(name)) {
return Collections.emptyList();
}
return this.headers.get(name);
}