Запрос с multipart / form-data возвращает ошибку 415 - PullRequest
0 голосов
/ 23 февраля 2020

Мне нужно получить этот запрос, используя Spring:

POST /test HTTP/1.1
user-agent: Dart/2.8 (dart:io)
content-type: multipart/form-data; boundary=--dio-boundary-3791459749
accept-encoding: gzip
content-length: 151
host: 192.168.0.107:8443

----dio-boundary-3791459749
content-disposition: form-data; name="MyModel"

{"testString":"hello world"}
----dio-boundary-3791459749--

Но, к сожалению, эта конечная точка Spring:

@PostMapping(value = "/test", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void test(@Valid @RequestPart(value = "MyModel") MyModel myModel) {
    String testString = myModel.getTestString();
}

возвращает 415 ошибка:

Content type 'multipart/form-data;boundary=--dio-boundary-2534440849' not supported

клиенту.

И это (та же конечная точка, но с consumes = MULTIPART_FORM_DATA_VALUE):

@PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void test(@Valid @RequestPart(value = "MyModel") MyModel myModel) {
    String testString = myModel.getTestString();
}

снова возвращает 415, но с этим сообщением:

Content type 'application/octet-stream' not supported

Я уже успешно использовал эту конечную точку (даже без consumes) с этим старым запросом:

POST /test HTTP/1.1
Content-Type: multipart/form-data; boundary=62b81b81-05b1-4287-971b-c32ffa990559
Content-Length: 275
Host: 192.168.0.107:8443
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.8.0

--62b81b81-05b1-4287-971b-c32ffa990559
Content-Disposition: form-data; name="MyModel"
Content-Transfer-Encoding: binary
Content-Type: application/json; charset=UTF-8
Content-Length: 35

{"testString":"hello world"}
--62b81b81-05b1-4287-971b-c32ffa990559--

Но, к сожалению, теперь мне нужно использовать первый описанный запрос, и я не могу добавьте в него дополнительные поля.

Итак, мне нужно изменить конечную точку Spring, но как?

1 Ответ

2 голосов
/ 23 февраля 2020

Вам нужно, чтобы ваш метод контроллера потреблял MediaType.MULTIPART_FORM_DATA_VALUE,

@PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
......

Вам также необходимо добавить MappingJackson2HttpMessageConverter поддержку application/octet-stream. В этом ответе

  • я настраиваю его с помощью WebMvcConfigurer#extendMessageConverters, чтобы сохранить конфигурацию по умолчанию для других преобразователей (Spring MVC настроен с преобразователями Spring Boot).
  • Я создаю конвертер из экземпляра ObjectMapper, используемого Spring.

[Для получения дополнительной информации]
Справочная документация по загрузке Spring - Spring MVC Автоконфигурация
Как получить Jackson ObjectMapper, используемый Spring 4.1?
Почему Spring Boot изменяет формат ответа JSON, даже если пользовательский преобразователь никогда не выполняет ручки JSON настроены?

@Configuration
public class MyConfigurer implements WebMvcConfigurer {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

        ReadOnlyMultipartFormDataEndpointConverter converter = new ReadOnlyMultipartFormDataEndpointConverter(
                objectMapper);
        List<MediaType> supportedMediaTypes = new ArrayList<>();
        supportedMediaTypes.addAll(converter.getSupportedMediaTypes());
        supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
        converter.setSupportedMediaTypes(supportedMediaTypes);

        converters.add(converter);
    }

}

[ПРИМЕЧАНИЕ]
Также вы можете изменить поведение вашего конвертера, расширив его.
В этом ответе я расширяю MappingJackson2HttpMessageConverter так что

  • читает данные только тогда, когда метод сопоставленного контроллера потребляет только MediaType.MULTIPART_FORM_DATA_VALUE
  • , он не записывает никакого ответа (другой конвертер делает это).
public class ReadOnlyMultipartFormDataEndpointConverter extends MappingJackson2HttpMessageConverter {

    public ReadOnlyMultipartFormDataEndpointConverter(ObjectMapper objectMapper) {
        super(objectMapper);
    }

    @Override
    public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
        // When a rest client(e.g. RestTemplate#getForObject) reads a request, 'RequestAttributes' can be null.
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return false;
        }
        HandlerMethod handlerMethod = (HandlerMethod) requestAttributes
                .getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
        if (handlerMethod == null) {
            return false;
        }
        RequestMapping requestMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
        if (requestMapping == null) {
            return false;
        }
        // This converter reads data only when the mapped controller method consumes just 'MediaType.MULTIPART_FORM_DATA_VALUE'.
        if (requestMapping.consumes().length != 1
                || !MediaType.MULTIPART_FORM_DATA_VALUE.equals(requestMapping.consumes()[0])) {
            return false;
        }
        return super.canRead(type, contextClass, mediaType);
    }

//      If you want to decide whether this converter can reads data depending on end point classes (i.e. classes with '@RestController'/'@Controller'),
//      you have to compare 'contextClass' to the type(s) of your end point class(es).
//      Use this 'canRead' method instead.
//      @Override
//      public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
//          return YourEndpointController.class == contextClass && super.canRead(type, contextClass, mediaType);
//      }

    @Override
    protected boolean canWrite(MediaType mediaType) {
        // This converter is only be used for requests.
        return false;
    }
}



Причины из 415 ошибок

Когда ваш метод контроллера потребляет MediaType.APPLICATION_OCTET_STREAM_VALUE, он не обрабатывает запрос с Content-Type: multipart/form-data;. Поэтому вы получаете 415.

С другой стороны, когда ваш метод контроллера потребляет MediaType.MULTIPART_FORM_DATA_VALUE, он может обработать запрос с помощью Content-Type: multipart/form-data;. Однако JSON без Content-Type не обрабатывается в зависимости от вашей конфигурации.
Когда вы аннотируете аргумент метода аннотацией @RequestPart,

  • RequestPartMethodArgumentResolver анализирует запрос.
  • RequestPartMethodArgumentResolver распознает тип содержимого как application/octet-stream, если он не указан.
  • RequestPartMethodArgumentResolver использует MappingJackson2HttpMessageConverter для анализа тела запроса и получения JSON.
  • По умолчанию MappingJackson2HttpMessageConverter поддерживает только application / json и application / * + json.
  • (насколько я прочитал ваш вопрос) Ваши MappingJackson2HttpMessageConverter s не кажутся поддержать application/octet-stream. (следовательно, вы получите 415.)



Заключение

Поэтому я думаю, что вы можете успешно справиться запрос, позволяющий MappingJackson2HttpMessageConverter (реализация HttpMessageConverter) поддерживать application/octet-stream, как указано выше.


[ОБНОВЛЕНИЕ 1]

Если вам не нужно проверять MyModel с аннотацией @Valid и просто хотите преобразовать JSON Тело к MyModel, @RequestParam может быть полезным.
Если вы выберете это решение, вы НЕ должны настроить MappingJackson2HttpMessageConverter для поддержки application/octet-stream.
Вы не можете справиться с только данные JSON, а также данные файла с использованием этого решения.

@PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void test(@RequestParam(value = "MyModel") Part part) throws IOException {

    // 'part' is an instance of 'javax.servlet.http.Part'.
    // According to javadoc of 'javax.servlet.http.Part',
    // 'The part may represent either an uploaded file or form data'

    try (InputStream is = part.getInputStream()) {
        ObjectMapper objectMapper = new ObjectMapper();
        MyModel myModel = objectMapper.readValue(part.getInputStream(), MyModel.class);

        .....
    }
    .....
}

См. также

Javado c RequestPartMethodArgumentResolver
Javado c из MappingJackson2HttpMessageConverter
Пустой тип содержимого не поддерживается (Смежный вопрос)
Spring Web MVC - Multipart

...