Spring: Возвращение пустых HTTP-ответов с ResponseEntity <Void> не работает

Мы внедряем REST API с Spring (4.1.1.). Для некоторых запросов HTTP мы хотели бы вернуть голову без тела в качестве ответа. Однако использование ResponseEntity<Void> не работает. При вызове с тегом MockMvc возвращается 406 (недопустимо). Использование ResponseEntity<String> без значения параметра (new ResponseEntity<String>( HttpStatus.NOT_FOUND )) отлично работает.

Метод:

@RequestMapping( method = RequestMethod.HEAD, value = Constants.KEY )
public ResponseEntity<Void> taxonomyPackageExists( @PathVariable final String key ) {

    LOG.debug( "taxonomyPackageExists queried with key: {0}", key ); //$NON-NLS-1$

    final TaxonomyKey taxonomyKey = TaxonomyKey.fromString( key );

    LOG.debug( "Taxonomy key created: {0}", taxonomyKey ); //$NON-NLS-1$

    if ( this.xbrlInstanceValidator.taxonomyPackageExists( taxonomyKey ) ) {

        LOG.debug( "Taxonomy package with key: {0} exists.", taxonomyKey ); //$NON-NLS-1$

        return new ResponseEntity<Void>( HttpStatus.OK );

    } else {

        LOG.debug( "Taxonomy package with key: {0} does NOT exist.", taxonomyKey ); //$NON-NLS-1$

        return new ResponseEntity<Void>( HttpStatus.NOT_FOUND );
    }

}

Тестовая версия (TestNG):

public class TaxonomyQueryControllerTest {

private XbrlInstanceValidator   xbrlInstanceValidatorMock;
private TaxonomyQueryController underTest;
private MockMvc                 mockMvc;

@BeforeMethod
public void setUp() {
    this.xbrlInstanceValidatorMock = createMock( XbrlInstanceValidator.class );
    this.underTest = new TaxonomyQueryController( this.xbrlInstanceValidatorMock );
    this.mockMvc = MockMvcBuilders.standaloneSetup( this.underTest ).build();
}

@Test
public void taxonomyPackageDoesNotExist() throws Exception {
    // record
    expect( this.xbrlInstanceValidatorMock.taxonomyPackageExists( anyObject( TaxonomyKey.class ) ) ).andStubReturn(
            false );

    // replay
    replay( this.xbrlInstanceValidatorMock );

    // do the test
    final String taxonomyKey = RestDataFixture.taxonomyKeyString;

    this.mockMvc.perform( head( "/taxonomypackages/{key}", taxonomyKey ).accept( //$NON-NLS-1$
            MediaType.APPLICATION_XML ) ).andExpect( status().isNotFound() );

}

}

Не удается выполнить трассировку стека:

FAILED: taxonomyPackageDoesNotExist
java.lang.AssertionError: Status expected:<404> but was:<406>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:60)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:89)
at org.springframework.test.web.servlet.result.StatusResultMatchers$10.match(StatusResultMatchers.java:652)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:153)
at de.zeb.control.application.xbrlstandalonevalidator.restservice.TaxonomyQueryControllerTest.taxonomyPackageDoesNotExist(TaxonomyQueryControllerTest.java:54)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:84)
at org.testng.internal.Invoker.invokeMethod(Invoker.java:714)
at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:901)
at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1231)
at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:127)
at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:111)
at org.testng.TestRunner.privateRun(TestRunner.java:767)
at org.testng.TestRunner.run(TestRunner.java:617)
at org.testng.SuiteRunner.runTest(SuiteRunner.java:334)
at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:329)
at org.testng.SuiteRunner.privateRun(SuiteRunner.java:291)
at org.testng.SuiteRunner.run(SuiteRunner.java:240)
at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86)
at org.testng.TestNG.runSuitesSequentially(TestNG.java:1224)
at org.testng.TestNG.runSuitesLocally(TestNG.java:1149)
at org.testng.TestNG.run(TestNG.java:1057)
at org.testng.remote.RemoteTestNG.run(RemoteTestNG.java:111)
at org.testng.remote.RemoteTestNG.initAndRun(RemoteTestNG.java:204)
at org.testng.remote.RemoteTestNG.main(RemoteTestNG.java:175)

Ответ 1

Когда вы возвращаете ResponseEntity без тела, Spring использует аргумент типа, предоставленный в объявлении типа возврата ResponseEntity выбора типа тела.

Таким образом, для

public ResponseEntity<Void> taxonomyPackageExists( @PathVariable final String key ) {

этот тип будет Void. Spring затем перебирает все свои зарегистрированные экземпляры HttpMessageConverter и находит тот, который может написать тело для типа Void. Поскольку такого HttpMessageConverter существует (для конфигурации по умолчанию), он решит, что не может HttpMessageConverter приемлемый ответ, и поэтому вернет HTTP-ответ 406 Not Acceptable.

С ResponseEntity<String> Spring будет использовать String в качестве тела ответа и найдет StringHttpMessageConverter в качестве обработчика. А поскольку StringHttpMessageHandler может создавать контент для любого типа носителя Accepted, он сможет обрабатывать application/xml, которое запрашивает ваш клиент.

В решении iddy85 (которое в настоящее время неверно, но, похоже, предлагает ResponseEntity<?>), Тип для тела будет выведен как Object. Если у вас есть правильные библиотеки в вашем classpath, Spring получит доступ к XML HttpMessageConverter который он может использовать для создания application/xml для типа Object.

Ответ 2

Ваша реализация метода неоднозначна, попробуйте следующее, немного измените свой код и используйте HttpStatus.NO_CONTENT i.e 204 No Content, как вместо HttpStatus.OK

Сервер выполнил запрос, но ему не нужно возвращать сущность-тело и может захотеть вернуть обновленную метаинформацию. ответ МОЖЕТ включать новую или обновленную метаинформацию в форме сущности-заголовки, которые, если они ДОЛЖНЫ быть связаны с запрошенный вариант.

Любое значение T будет игнорироваться для 204, но не для 404

  public ResponseEntity<?> taxonomyPackageExists( @PathVariable final String key ) {
            LOG.debug( "taxonomyPackageExists queried with key: {0}", key ); //$NON-NLS-1$
            final TaxonomyKey taxonomyKey = TaxonomyKey.fromString( key );
            LOG.debug( "Taxonomy key created: {0}", taxonomyKey ); //$NON-NLS-1$

            if ( this.xbrlInstanceValidator.taxonomyPackageExists( taxonomyKey ) ) {
                LOG.debug( "Taxonomy package with key: {0} exists.", taxonomyKey ); //$NON-NLS-1$
                 return new ResponseEntity<T>(HttpStatus.NO_CONTENT);
            } else {
               LOG.debug( "Taxonomy package with key: {0} does NOT exist.", taxonomyKey ); //$NON-NLS-1$
                return new ResponseEntity<T>( HttpStatus.NOT_FOUND );
            }

    }

Ответ 3

Вы также не можете указать параметр типа, который кажется немного чище, и что задумал Spring при просмотре документов:

@RequestMapping(method = RequestMethod.HEAD, value = Constants.KEY )
public ResponseEntity taxonomyPackageExists( @PathVariable final String key ){
    // ...
    return new ResponseEntity(HttpStatus.NO_CONTENT);
}

Ответ 4

В соответствии с Spring 4 MVC ResponseEntity.BodyBuilder и примером расширений ResponseEntity его можно записать так:

....
   return ResponseEntity.ok().build();
....
   return ResponseEntity.noContent().build();

ОБНОВИТЬ:

Если возвращаемое значение является Optional есть удобный метод, возвращаемый ok() или notFound():

return ResponseEntity.of(optional)

Ответ 5

Лично, чтобы иметь дело с пустыми ответами, я использую в своих Интеграционных тестах объект MockMvcResponse следующим образом:

MockMvcResponse response = RestAssuredMockMvc.given()
                .webAppContextSetup(webApplicationContext)
                .when()
                .get("/v1/ticket");

    assertThat(response.mockHttpServletResponse().getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());

и в моем контроллере я возвращаю пустой ответ в конкретном случае, как это:

return ResponseEntity.noContent().build();