Как в Spring Boot включить Content-Length в ответ?
От: Worminator X Россия #StandWithPalestine 🖤🤍💚
Дата: 06.05.22 10:01
Оценка:
Разрабатываю онлайн-игру с сервером на Java и клиентом на Си. Для передачи данных используется REST + XML (Jackson). Клиент подключается к серверу через TCP сокет, отправляет HTTP-запросы, парсит тело из ответа с помощью Expat'а и обновляет картинку. На сервере используется обычное Spring Boot приложение со встроенным Jetty.

Проблема в том, что по умолчанию Spring Boot использует Transfer-Encoding с разбиением ответа на chunk'и. Возвращаемые заголовки в ответе:

Content-Type: application/xml
Date: Fri, 06 May 2022 09:41:32 GMT
Transfer-Encoding: chunked

Естественно, читать такой ответ, собирая из кусков, неудобно и очень медленно. Как из заголовков убрать Transfer-Encoding: chunked и соответственно добавить Content-Length с указанием размера данных, чтобы тело шло единым набором байтов, который сразу можно было бы прочитать?

Вот пример контроллера Spring:
@RestController
@RequestMapping("/users")
public class UsersController {
    @GetMapping(value = "/")
    public List<User> getAllUsers() {
        return UsersService.getInstance().findAll();
    }

    @GetMapping(value = "/{id}")
    public ResponseEntity<User> getUser(@PathVariable String id) {
        final Optional<User> user = UsersService.getInstance().find(id);
        return user.isPresent() ?
            ResponseEntity.ok().contentType(MediaType.APPLICATION_XML).body(user.get()) :
            ResponseEntity.notFound().build();
    }
}


На StackOverflow пишут, что это Spring таким образом использует сжатие ответа. Хорошо, в файле конфигурации application.yml отключаю его:

server:
    compression:
        enabled: false
    port: 2992


Не помогает, все равно ответ возвращается с Transfer-Encoding: chunked. Как исправить?

UPD Работает, если задать Content-Length в контроллере вручную через метод ReponseEntity:
@RestController
@RequestMapping("/users")
public class UsersController {
    @GetMapping(value = "/")
    public List<User> getAllUsers() {
        return UsersService.getInstance().findAll();
    }

    @GetMapping(value = "/{id}")
    public ResponseEntity<User> getUser(@PathVariable String id) {
        final Optional<User> user = UsersService.getInstance().find(id);
        return user.isPresent() ?
            ResponseEntity.ok().contentType(MediaType.APPLICATION_XML).contentLength(
                getContentLength(user.get())
            ).body(user.get()) :
            ResponseEntity.notFound().build();
    }

    private static long getContentLength(User user) {
        try {
            final XmlMapper mapper = new XmlMapper();
            final String xml = mapper.writeValueAsString(user);
            return xml.length();
        } catch (JsonProcessingException ex) {
            return 0;
        }            
    }
}

Но это костыль и быдлокод, 2 раза выполяется сериализация для объекта. Очевидно, Spring должен рассчитывать его автоматически. Как это сделать?
Как запру я тебя за железный замок, за дубовую дверь окованную,
Чтоб свету божьего ты не видела, мое имя честное не порочила…
М. Лермонтов. Песня про царя Ивана Васильевича, молодого опричника и удалого купца Калашникова
Отредактировано 06.05.2022 10:54 Worminator X . Предыдущая версия .
Re: Как в Spring Boot включить Content-Length в ответ?
От: Pzz Россия https://github.com/alexpevzner
Дата: 06.05.22 10:44
Оценка: +1
Здравствуйте, Worminator X, Вы писали:

WX>Проблема в том, что по умолчанию Spring Boot использует Transfer-Encoding с разбиением ответа на chunk'и. Возвращаемые заголовки в ответе:


Ничего не понимаю в твоей Яве, но сразу скажу, что chunked transfer encoding используется тогда, когда размер тела сообщения заранее не известен.

И дам я тебе так же добрый совет. Не пиши своего HTTP-клиента на Си. Возьми готовую библиотеку, CURL, например. Оно само за тебя справится с encoding'ами и прочими нюансами, о которых ты даже не знаешь. Это я тебе говорю как человек, который сам несколько таких клиентов написал, причем вполне работоспособных
Re[2]: Как в Spring Boot включить Content-Length в ответ?
От: Worminator X Россия #StandWithPalestine 🖤🤍💚
Дата: 06.05.22 10:59
Оценка:
Здравствуйте, Pzz, Вы писали:

Pzz>И дам я тебе так же добрый совет. Не пиши своего HTTP-клиента на Си. Возьми готовую библиотеку, CURL, например. Оно само за тебя справится с encoding'ами и прочими нюансами, о которых ты даже не знаешь. Это я тебе говорю как человек, который сам несколько таких клиентов написал, причем вполне работоспособных


В смысле libcurl? Она же вроде только для UNIX?

Нужно что-то кроссплатформенное (Windows и Linux как минимум, а лучше и Android). И не C++, его я не знаю. Чтобы подключить к готовому игровому 2D движку на SDL.
Как запру я тебя за железный замок, за дубовую дверь окованную,
Чтоб свету божьего ты не видела, мое имя честное не порочила…
М. Лермонтов. Песня про царя Ивана Васильевича, молодого опричника и удалого купца Калашникова
Re: Как в Spring Boot включить Content-Length в ответ?
От: GarryIV  
Дата: 06.05.22 11:22
Оценка:
Здравствуйте, Worminator X, Вы писали:

WX>Естественно, читать такой ответ, собирая из кусков, неудобно и очень медленно.


Я нихрена не понял кому это неудобно. Это как бы внутре http клиента прозрачно делается.
Почему это вдруг медленно тоже не особо понятно. Бенчмарки делал?

Но если прям вот надо можно так https://stackoverflow.com/a/37749537
WBR, Igor Evgrafov
Re: Как в Spring Boot включить Content-Length в ответ?
От: Micht  
Дата: 06.05.22 11:23
Оценка:
Здравствуйте, Worminator X, Вы писали:

WX> @GetMapping(value = "/{id}")

WX> public ResponseEntity<User> getUser(@PathVariable String id) {
WX> final Optional<User> user = UsersService.getInstance().find(id);
WX> return user.isPresent() ?
WX> ResponseEntity.ok().contentType(MediaType.APPLICATION_XML).body(user.get()) :
WX> ResponseEntity.notFound().build();
WX> }

99% это из-за ResponseEntity (тем более, как ниже ответили, такое бывает, когда заранее неизвестна длина ответа). Я бы предложил

@GetMapping(value = "/{id}")
public ResponseEntity<User> getUser(@PathVariable String id) {
return UsersService.getInstance().find(id).orElseThrow(() -> new MyNotFoundException("user not found: " + id));
}


И обработчик, посмотри в соседней теме про валидацию данных.
Отредактировано 06.05.2022 11:41 Micht . Предыдущая версия .
Re[3]: Как в Spring Boot включить Content-Length в ответ?
От: Pzz Россия https://github.com/alexpevzner
Дата: 06.05.22 11:26
Оценка:
Здравствуйте, Worminator X, Вы писали:

Pzz>>И дам я тебе так же добрый совет. Не пиши своего HTTP-клиента на Си. Возьми готовую библиотеку, CURL, например. Оно само за тебя справится с encoding'ами и прочими нюансами, о которых ты даже не знаешь. Это я тебе говорю как человек, который сам несколько таких клиентов написал, причем вполне работоспособных


WX>В смысле libcurl? Она же вроде только для UNIX?


Нет, она не только для UNIX. https://curl.se/windows/

Есть еще mingw'шные сборки.

HTTP только кажется простым. Там много тонкостей и нюансов. Не связывайся, пока не приперло, бери готовое.
Re: Как в Spring Boot включить Content-Length в ответ?
От: · Великобритания  
Дата: 06.05.22 11:40
Оценка:
Здравствуйте, Worminator X, Вы писали:

WX> final String xml = mapper.writeValueAsString(user);

WX> return xml.length();
Здесь у тебя будет размер в символах, а Content-Length должен быть в байтах. Это не всегда одно и то же.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re: Как в Spring Boot включить Content-Length в ответ?
От: maxkar  
Дата: 06.05.22 11:42
Оценка:
Здравствуйте, Worminator X, Вы писали:

WX>Клиент подключается к серверу через TCP сокет, отправляет HTTP-запросы,

Какую версию HTTP заявляет клиент в запросе? Если 1.1, то это его проблемы, Chunked должно поддерживаться. Если не поддерживается, то нужно завлять 1.0 и не заявлять поддержку chunked в заголовке TE.
Re: Как в Spring Boot включить Content-Length в ответ?
От: · Великобритания  
Дата: 06.05.22 11:52
Оценка:
Здравствуйте, Worminator X, Вы писали:

WX>Но это костыль и быдлокод, 2 раза выполяется сериализация для объекта. Очевидно, Spring должен рассчитывать его автоматически. Как это сделать?

Магии тут не бывает. Чтобы узнать размер объекта в сериализованном виде — его нужно сериализовать. Единственное, что можно найти готовый компонент, а не свой кривой костыль.
Это уже пробовал? https://stackoverflow.com/questions/24156490/how-to-set-content-length-in-spring-mvc-rest-for-json
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[2]: Как в Spring Boot включить Content-Length в ответ?
От: Worminator X Россия #StandWithPalestine 🖤🤍💚
Дата: 06.05.22 11:58
Оценка:
Здравствуйте, GarryIV, Вы писали:

GIV>Я нихрена не понял кому это неудобно. Это как бы внутре http клиента прозрачно делается.


Так клиент я пишу сам на сокетах.

GIV>Почему это вдруг медленно тоже не особо понятно. Бенчмарки делал?


Запрос должен отправляться несколько раз в секунду (пока запланировано 50), для обновления игрового состояния. С Transfer-Encoding: chunked:
1) читаем размер chuck'а
2) если не 0, то читаем chuck в буфер, сдвигаем указатель
3) читаем размер следующего chunk'а
4) если не 0, то читаем следующий chunk и т.д.

Нужно собрать тело по кускам. Это очевидно медленно. С заданием Content-Length:
1) получаем размер данных из Content-Length
2) читаем тело указанного размера и сразу можно его парсить

GIV>Но если прям вот надо можно так https://stackoverflow.com/a/37749537


Там аналогично моему 2-му варианту, вручную задается Content-Length (и, как отметили, не совсем правильно):
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.CONTENT_LENGTH, String.valueOf(new ObjectMapper().writeValueAsString(map).length()));
return new ResponseEntity<Map<String, ContactInfo>>(map, headers, HttpStatus.CREATED);

Должен быть какой-то способ, чтобы Spring его вычислял и добавлял сам, вместо Transfer-Encoding: chunked.
Как запру я тебя за железный замок, за дубовую дверь окованную,
Чтоб свету божьего ты не видела, мое имя честное не порочила…
М. Лермонтов. Песня про царя Ивана Васильевича, молодого опричника и удалого купца Калашникова
Re[2]: Как в Spring Boot включить Content-Length в ответ?
От: Worminator X Россия #StandWithPalestine 🖤🤍💚
Дата: 06.05.22 12:01
Оценка:
Здравствуйте, maxkar, Вы писали:

M>Какую версию HTTP заявляет клиент в запросе? Если 1.1, то это его проблемы, Chunked должно поддерживаться. Если не поддерживается, то нужно завлять 1.0 и не заявлять поддержку chunked в заголовке TE.


С клиента посылается запрос:
static char *request = "GET /users/1 HTTP/1.1\r\nHost: 127.0.0.1:2992\r\nUser-Agent: GameClient (Windows/x64)\r\n\r\n";


Если отправлять GET /users/1 HTTP/1.0..., то приходит ответ без chunked и без Content-Length (тоже неудобно, нужно знать размер тела).
Как запру я тебя за железный замок, за дубовую дверь окованную,
Чтоб свету божьего ты не видела, мое имя честное не порочила…
М. Лермонтов. Песня про царя Ивана Васильевича, молодого опричника и удалого купца Калашникова
Отредактировано 06.05.2022 12:15 Worminator X . Предыдущая версия .
Re[4]: Как в Spring Boot включить Content-Length в ответ?
От: Worminator X Россия #StandWithPalestine 🖤🤍💚
Дата: 06.05.22 12:08
Оценка:
Здравствуйте, Pzz, Вы писали:

Pzz>Нет, она не только для UNIX. https://curl.se/windows/


Хорошо, попробуем.

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


Вот сейчас раздумываю, стоит ли вообще использовать HTTP. Может, лучше перейти на WebSocket? Конечно, REST сервис писать проще.
Как запру я тебя за железный замок, за дубовую дверь окованную,
Чтоб свету божьего ты не видела, мое имя честное не порочила…
М. Лермонтов. Песня про царя Ивана Васильевича, молодого опричника и удалого купца Калашникова
Re[5]: Как в Spring Boot включить Content-Length в ответ?
От: Pzz Россия https://github.com/alexpevzner
Дата: 06.05.22 12:11
Оценка:
Здравствуйте, Worminator X, Вы писали:

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


WX>Вот сейчас раздумываю, стоит ли вообще использовать HTTP. Может, лучше перейти на WebSocket? Конечно, REST сервис писать проще.


WebSocket — это HTTP плюс еще полстолькоже. И готовая реализация на Си мне не известна (это не значит, что ее нет).
Re[3]: Как в Spring Boot включить Content-Length в ответ?
От: maxkar  
Дата: 06.05.22 12:13
Оценка:
Здравствуйте, Worminator X, Вы писали:

WX>С клиента посылается запрос:

WX>
static char *request = "GET /users/1 HTTP/1.1\r\nHost: 127.0.0.1:2992\r\nUser-Agent: GameClient (Windows/x64)\r\n\r\n";


Ну вот же сами написали (я выделил). Назвался 1.1 — будь добр поддерживать Chunked. Сервер его вообще по рандому может выставлять. И если вдруг у вас когда-нибудь появятся маршрутизаторы или балансировщики нагрузки, они тоже могут это делать. Начните с версии 1.0 и потом разбирайтесь с поступающими проблемами. Есть шансы, что просто весрии уже будет достаточно.
Re[3]: Как в Spring Boot включить Content-Length в ответ?
От: Pzz Россия https://github.com/alexpevzner
Дата: 06.05.22 12:16
Оценка:
Здравствуйте, Worminator X, Вы писали:

WX>Запрос должен отправляться несколько раз в секунду (пока запланировано 50), для обновления игрового состояния. С Transfer-Encoding: chunked:

WX>1) читаем размер chuck'а
WX>2) если не 0, то читаем chuck в буфер, сдвигаем указатель
WX>3) читаем размер следующего chunk'а
WX>4) если не 0, то читаем следующий chunk и т.д.

Читаешь в буфер, сколько прочлось, потом парсишь in place. Это не шибко медленно, но это муторно. Особенно с учетом того, что при очередном чтении может прочитаться половина размера chunk'а, а следущее чтение принесет вторую половину.

Когда тебе к этому хозяйству приспичит добавить, HTTPS, gzip и поддержку редиректов и авторизации, ты пожалеешь, что связался
Re[4]: Как в Spring Boot включить Content-Length в ответ?
От: Worminator X Россия #StandWithPalestine 🖤🤍💚
Дата: 06.05.22 12:19
Оценка:
Здравствуйте, maxkar, Вы писали:

M>Ну вот же сами написали (я выделил). Назвался 1.1 — будь добр поддерживать Chunked. Сервер его вообще по рандому может выставлять. И если вдруг у вас когда-нибудь появятся маршрутизаторы или балансировщики нагрузки, они тоже могут это делать. Начните с версии 1.0 и потом разбирайтесь с поступающими проблемами. Есть шансы, что просто весрии уже будет достаточно.


Spring ведет себя странно, отправляет ответ все равно HTTP/1.1 200 OK. И без Content-Length. Как-то нужно его включать именно на сервере.
Как запру я тебя за железный замок, за дубовую дверь окованную,
Чтоб свету божьего ты не видела, мое имя честное не порочила…
М. Лермонтов. Песня про царя Ивана Васильевича, молодого опричника и удалого купца Калашникова
Re[2]: Как в Spring Boot включить Content-Length в ответ?
От: Worminator X Россия #StandWithPalestine 🖤🤍💚
Дата: 06.05.22 12:25
Оценка:
Здравствуйте, ·, Вы писали:

·>Магии тут не бывает. Чтобы узнать размер объекта в сериализованном виде — его нужно сериализовать. Единственное, что можно найти готовый компонент, а не свой кривой костыль.

·>Это уже пробовал? https://stackoverflow.com/questions/24156490/how-to-set-content-length-in-spring-mvc-rest-for-json

Изучаю, пока не очень понятно.
Как запру я тебя за железный замок, за дубовую дверь окованную,
Чтоб свету божьего ты не видела, мое имя честное не порочила…
М. Лермонтов. Песня про царя Ивана Васильевича, молодого опричника и удалого купца Калашникова
Re[3]: Как в Spring Boot включить Content-Length в ответ?
От: Stanislav V. Zudin Россия  
Дата: 06.05.22 12:56
Оценка:
Здравствуйте, Worminator X, Вы писали:

WX>В смысле libcurl? Она же вроде только для UNIX?


WX>Нужно что-то кроссплатформенное (Windows и Linux как минимум, а лучше и Android). И не C++, его я не знаю. Чтобы подключить к готовому игровому 2D движку на SDL.


Тогда посмотри Poco.
_____________________
С уважением,
Stanislav V. Zudin
Re: Как в Spring Boot включить Content-Length в ответ?
От: vsb Казахстан  
Дата: 06.05.22 12:59
Оценка: +2
Для этого надо реализовать Servlet Filter (если у вас приложение на сервлетах). В нём надо в буфер складывать записываемые данные и в конце поставить content-length и после этого выдать буфер.
Re[4]: Как в Spring Boot включить Content-Length в ответ?
От: Pzz Россия https://github.com/alexpevzner
Дата: 06.05.22 13:49
Оценка:
Здравствуйте, Stanislav V. Zudin, Вы писали:

WX>>Нужно что-то кроссплатформенное (Windows и Linux как минимум, а лучше и Android). И не C++, его я не знаю. Чтобы подключить к готовому игровому 2D движку на SDL.


SVZ>Тогда посмотри Poco.


Товарищ просил не на C++
Re[5]: Как в Spring Boot включить Content-Length в ответ?
От: Stanislav V. Zudin Россия  
Дата: 06.05.22 14:04
Оценка:
Здравствуйте, Pzz, Вы писали:

SVZ>>Тогда посмотри Poco.


Pzz>Товарищ просил не на C++


На Сях такое программить это форменный мазохизм.
_____________________
С уважением,
Stanislav V. Zudin
Re[6]: Как в Spring Boot включить Content-Length в ответ?
От: Pzz Россия https://github.com/alexpevzner
Дата: 06.05.22 14:09
Оценка:
Здравствуйте, Stanislav V. Zudin, Вы писали:

Pzz>>Товарищ просил не на C++


SVZ>На Сях такое программить это форменный мазохизм.


Я пробовал.

В принципе, ничего такого, чтобы это это было сложно программировать именно на Сях, там нет. Но HTTP сам по себе сложнее, чем кажется, и это надо понимать прежде, чем ввязываешься. И не ввязываться без нужды
Re[3]: Как в Spring Boot включить Content-Length в ответ?
От: GarryIV  
Дата: 06.05.22 15:58
Оценка:
Здравствуйте, Worminator X, Вы писали:

GIV>>Я нихрена не понял кому это неудобно. Это как бы внутре http клиента прозрачно делается.

WX>Так клиент я пишу сам на сокетах.
А, ясно-понятно.

GIV>>Почему это вдруг медленно тоже не особо понятно. Бенчмарки делал?

WX>Нужно собрать тело по кускам. Это очевидно медленно.
Вопросов больше не имею.
WBR, Igor Evgrafov
Re[5]: Как в Spring Boot включить Content-Length в ответ?
От: maxkar  
Дата: 06.05.22 17:38
Оценка:
Здравствуйте, Worminator X, Вы писали:

WX>Spring ведет себя странно, отправляет ответ все равно HTTP/1.1 200 OK. И без Content-Length. Как-то нужно его включать именно на сервере.


Не, не странно. Вполне логично. Ответ вполне удовлетворяет спецификации. Можно еще "Connection: Keep-Alive" в заголовках передавать, но это тот еще костыль и включает новые проблемы.

"Включить content-length на сервере" ручки нет, это тоже костыль и в стандартную поставку не входит. Spring/Jetty ведут себя правильно и согласно спецификации HTTP. Spring не вычисляет длину ответа, она ему не нужна. Он делает кучу магии для нахождения кодека, а потом делает что-то вроде encoder.encode(entity, servletResponse.getOutputStream()). Т.е. пишет ответ сразу в OutputStream, полученный из Jetty. В Jetty же в этом потоке есть буфер. Пока поступают новые данные и есть место в буфере, эти данные пишутся именно в буфер. Если обработка запроса завершается до того, как буфер заполнен, вы получаете Content-Length и данные. Интересности начинаются, если буфера не хватает. В этом случае сервер сбрасывает данные в TCP сокет. Для HTTP 1.1 это будет chunked и первый чанк. Для HTTP 1.0 это будет просто поток данных. В HTTP 1.0 допускается отправлять сообщение без content-length на соединениях без keep-alive. В этом случае закрытие сокета является индикатором конца данных.

Что можно делать. Можно подкрутить костыли. В Jetty есть HttpConfiguration. Там есть OutputBufferSize и OutputAggregationSize. Первый задает буфер, который используется до того, как первая порция данных будет послана. Что делает второй — я . Вполне вероятно, влияет на размер чанков. Можете поэкспериментировать с обоими. Как добраться до них через Spring я не в курсе, не пользуюсь фреймворками. Если делать на сервере, то vsb предложил правильное решение
Автор: vsb
Дата: 06.05.22
. Делать фильтр, в фильтре собирать сообщение и вручную устанавливать Content-Length. На клиенте тоже стоит взять готовый http-client или добавить поддержку сборки фрагментов (или для http 1.0 чтение сообщения до конца потока). На фоне сети это не должно быть "медленно". Или на худой конец можно обычные сокеты сделать (не вебсокеты).
Re[6]: Как в Spring Boot включить Content-Length в ответ?
От: vsb Казахстан  
Дата: 06.05.22 18:46
Оценка: 19 (2)
Это не совсем правильный ответ. Во-первых Spring работает не так. Если он может вычислить длину ответа, он его вычисляет и проставляет Content-Length. Например если контролер отдаёт строку, то спринг корректно вычислит Content-Length, хоть гигабайт отдавай. Если он не может вычислить, то он действительно просто пишет в ServletOutputStream. И при этом Jetty ничего не вычисляет ни при какой ситуации. Вот пример кода и как он работает:

@RestController
public class TestController {

    @JsonInclude(NON_NULL)
    public static class Test1Response {
        private String s;
    }

    @GetMapping("/test1")
    public ResponseEntity<?> getTest1() {
        return ResponseEntity.ok(new Test1Response());
    }

    @GetMapping("/test2")
    public String getTest2() {
        return Strings.repeat("1", 1024 * 1024 * 1024);
    }
}


% curl -v http://127.0.0.1:8080/test1
> GET /test1 HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.79.1
> Accept: */*
> 
< HTTP/1.1 200 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Fri, 06 May 2022 18:44:03 GMT
< 
{}                                                                                                 
% curl -v http://127.0.0.1:8080/test2 >/dev/null
> GET /test2 HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.79.1
> Accept: */*
> 
< HTTP/1.1 200 
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 1073741824
< Date: Fri, 06 May 2022 18:44:16 GMT
<


Конкретное место, которое в Spring отвечает за вычисление длины ответа это org.springframework.http.converter.AbstractHttpMessageConverter#getContentLength

У обработчика строкового типа этот метод реализован, у jackson — нет.
Отредактировано 06.05.2022 19:04 vsb . Предыдущая версия . Еще …
Отредактировано 06.05.2022 18:47 vsb . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.