用Chrome浏览器访问后端某流媒体资源接口,发现媒体资源内容无法加载,F12打开Network工具调试,发现资源报错net::ERR_HTTP2_PROTOCOL_ERROR,状态码又是200,表示客户端到服务器的链接是正常的,能建立正常链接,但是就是资源请求不回来。
然后谷歌搜索How To Fix the ERR_HTTP2_PROTOCOL_ERROR
一种观点认为需要配置nginx的proxy_max_temp_file_size
在Nginx的反向代理配置中,将proxy_max_temp_file_size
设置为0的含义是禁用临时文件的使用。默认情况下,Nginx会在反向代理过程中使用临时文件来存储大体积的响应数据。
当反向代理的响应数据超过proxy_max_temp_file_size
指定的大小时,Nginx会将响应数据写入到一个临时文件中,然后再将该文件发送给客户端。这是为了避免占用过多的内存,特别是在处理大文件时。
将proxy_max_temp_file_size
设置为0意味着禁用临时文件的使用。这样,当反向代理的响应数据超过指定的大小时,Nginx将会使用内存来存储响应数据,而不是写入到临时文件中。
禁用临时文件的使用可能会导致Nginx消耗更多的内存资源。因此,如果你的反向代理处理的响应数据通常较大,或者希望减少对磁盘I/O的依赖,可以考虑将proxy_max_temp_file_size
设置为0。但是,请注意确保Nginx服务器的内存足够支持处理大量的响应数据。
配置示例:
location / {
proxy_pass http://backend;
proxy_max_temp_file_size 0;
}
在上述示例中,将proxy_max_temp_file_size
设置为0以禁用临时文件的使用。这样,Nginx将会使用内存来存储反向代理的响应数据。
参考链接:https://www.cnblogs.com/ami-miao/p/15175129.html
一种观点认为要关闭谷歌浏览器的QUIC协议
chrome://flags/#enable-quic
Now you’ll see a highlighted result labeled Experimental QUIC protocol. For this setting, change it to Disabled:
实际上谷歌QUIC(Quick UDP Internet Connections)协议和HTTP/2是两种不同的协议,但它们之间有一些关联和相互影响。
HTTP/2是一种新的网络协议,旨在改善HTTP/1.1的性能和效率。它引入了一些新的特性,如二进制分帧、头部压缩、多路复用等,以减少延迟、提高吞吐量和性能。
谷歌QUIC协议是一种基于UDP的传输协议,用于在网络上进行快速、安全的数据传输。QUIC协议结合了传输层和应用层的特性,旨在提供更低的延迟和更高的性能。它使用UDP协议而不是TCP协议来进行数据传输,并集成了一些与安全相关的功能,如TLS加密和身份验证。
HTTP/2和QUIC之间的关系在于,QUIC协议可以作为HTTP/2的底层传输协议。这意味着在使用QUIC协议时,可以在其上运行HTTP/2,从而获得HTTP/2的性能优势和功能。
试了一下也没什么作用。
参考链接:How To Fix the ERR_HTTP2_PROTOCOL_ERROR
最后想了一下代码变动的内容:
因为增加了LoggingFilter,拦截请求打印日志,打印body里的数据,会出现不能再次读取的问题,servlet的requestbody以及response的body只能被读取一次,一旦流被读取了,就无法再次读取了。
推荐使用Spring本身提供的Wrapper类来解决此问题,当然也可以自己封装个Wrapper类,继承HttpServletRequestWrapper即可。
Spring ContentCachingRequestWrapper和ContentCachingResponseWrapper是用于缓存HTTP请求和响应内容的包装器类。
ContentCachingRequestWrapper用于包装HttpServletRequest对象,它可以缓存请求的内容,包括请求体和请求参数。通过使用ContentCachingRequestWrapper,可以在请求处理的任何时候获取请求的内容,例如日志记录或调试。
ContentCachingResponseWrapper用于包装HttpServletResponse对象,它可以缓存响应的内容,包括响应体和响应头。通过使用ContentCachingResponseWrapper,可以在响应返回到客户端之前获取响应的内容,例如记录响应的内容或修改响应的头部信息。
private static ContentCachingRequestWrapper wrapRequest(HttpServletRequest request) {
if (request instanceof ContentCachingRequestWrapper) {
return (ContentCachingRequestWrapper) request;
} else {
return new ContentCachingRequestWrapper(request);
}
}
private static ContentCachingResponseWrapper wrapResponse(HttpServletResponse response) {
if (response instanceof ContentCachingResponseWrapper) {
return (ContentCachingResponseWrapper) response;
} else {
return new ContentCachingResponseWrapper(response);
}
}
对了一下代码变更,然后查看了一下nginx报错的日志
upstream prematurely closed connection while reading upstream异常 返回ERR_CONTENT_LENGTH_MISMATCH
@ApiOperation(value = "获取资源媒体流", hidden = true)
@RequestMapping(value = "/getStream/*", method = RequestMethod.GET)
public ResponseEntity<StreamingResponseBody> getStream(@RequestHeader(required = false, value = "Range") String range,
@RequestParam("url") String url) {
return new ResponseEntity<>(streamingResponseBody, responseEntity.getHeaders(), StringUtils.isBlank(range) ? HttpStatus.OK : HttpStatus.PARTIAL_CONTENT);
}
当请求getStream/xxx.jpg资源的时候,经过LoggingFilter拦截之后,ContentCachingResponseWrapper包装后,最后执行response.copyBodyToResponse时,异步请求返回流数据,导致连接提前中断,最终获取的ResponseBody里面内容为空,所以请求就会不定时报ERR_HTTP2_PROTOCOL_ERROR错误,最后判断如果是异步请求或者请求流媒体资源就跳过doFilterWrapped。
if (isAsyncDispatch(request) || request.getRequestURI().contains("/getStream/")) {
filterChain.doFilter(request, response);
} else {
doFilterWrapped(wrapRequest(request), wrapResponse(response), filterChain);
}
参考链接:Not getting response body when using ContentCachingResponseWrapper
当然在stackoverflow上看到这样的解决方案
responseToCache.copyBodyToResponse()
更改成下面的代码:the execution order: sync-filterChain(in)->Controller of DeferredResult<VO> method -> sync-filterChain(out) -> async-filterChain(in) ->async-filterChain(out). It seems that the response returns to the front-end just after sync-filterChain(out), not after async-filterChain(out), so of cause the sync-response-body is empty!
if (requestToCache.isAsyncStarted()) {
requestToCache.getAsyncContext().addListener(new AsyncListener() {
public void onComplete(AsyncEvent asyncEvent) throws IOException {
responseToCache.copyBodyToResponse()
}
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
}
public void onError(AsyncEvent asyncEvent) throws IOException {
}
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
}
})
} else {
responseToCache.copyBodyToResponse()
}