@@ -436,17 +436,57 @@ public class TimiSpring {
|
||||
*/
|
||||
public static RequestRange getRequestRange(long total) throws IOException {
|
||||
HttpServletResponse resp = getResponse();
|
||||
|
||||
String range = getRequestAttrAsString("Range");
|
||||
if (range == null || !range.startsWith("bytes=")) {
|
||||
String range = getHeader("Range");
|
||||
if (TimiJava.isEmpty(range)) {
|
||||
return null;
|
||||
}
|
||||
range = range.trim();
|
||||
if (!range.startsWith("bytes=")) {
|
||||
return null;
|
||||
}
|
||||
String rangeValue = range.substring("bytes=".length()).trim();
|
||||
int separatorIndex = rangeValue.indexOf('-');
|
||||
// 当前实现只接受单个字节范围,不处理 multipart/byteranges
|
||||
if (separatorIndex < 0 || rangeValue.indexOf(',', separatorIndex + 1) != -1) {
|
||||
resp.setHeader("Content-Range", "bytes */" + total);
|
||||
resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
return null;
|
||||
}
|
||||
String startText = rangeValue.substring(0, separatorIndex).trim();
|
||||
String endText = rangeValue.substring(separatorIndex + 1).trim();
|
||||
if (total <= 0) {
|
||||
resp.setHeader("Content-Range", "bytes */" + total);
|
||||
resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
return null;
|
||||
}
|
||||
long start;
|
||||
long end;
|
||||
try {
|
||||
if (TimiJava.isEmpty(startText)) {
|
||||
if (TimiJava.isEmpty(endText)) {
|
||||
resp.setHeader("Content-Range", "bytes */" + total);
|
||||
resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
return null;
|
||||
}
|
||||
// `bytes=-500` 表示请求文件末尾 500 字节
|
||||
long suffixLength = Long.parseLong(endText);
|
||||
if (suffixLength <= 0) {
|
||||
resp.setHeader("Content-Range", "bytes */" + total);
|
||||
resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
return null;
|
||||
}
|
||||
start = Math.max(0, total - suffixLength);
|
||||
end = total - 1;
|
||||
} else {
|
||||
// `bytes=97043805-` 表示从指定起点一直读到文件结尾
|
||||
start = Long.parseLong(startText);
|
||||
end = TimiJava.isEmpty(endText) ? total - 1 : Long.parseLong(endText);
|
||||
}
|
||||
} catch (NumberFormatException exception) {
|
||||
resp.setHeader("Content-Range", "bytes */" + total);
|
||||
resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
return null;
|
||||
}
|
||||
// 处理 bytes=0-999 格式
|
||||
String rangeValue = range.substring("bytes=".length());
|
||||
String[] ranges = rangeValue.split("-");
|
||||
TimiException.requiredTrue(2 == ranges.length, "Invalid Range format");
|
||||
long start = Long.parseLong(ranges[0]);
|
||||
long end = ranges[1].isEmpty() ? total - 1 : Long.parseLong(ranges[1]);
|
||||
// 验证范围有效性
|
||||
if (start < 0 || total <= end || end < start) {
|
||||
resp.setHeader("Content-Range", "bytes */" + total);
|
||||
@@ -472,6 +512,9 @@ public class TimiSpring {
|
||||
public static void responseRangeStream(InputStream stream, long total) throws IOException {
|
||||
HttpServletResponse resp = getResponse();
|
||||
RequestRange range = getRequestRange(total);
|
||||
if (range == null && resp.isCommitted()) {
|
||||
return;
|
||||
}
|
||||
if (range == null) {
|
||||
// 完整文件
|
||||
resp.setContentLengthLong(total);
|
||||
|
||||
@@ -3,7 +3,10 @@ package com.imyeyu.spring.util;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.java.bean.timi.TimiResponse;
|
||||
import com.imyeyu.spring.TimiSpring;
|
||||
import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.ValidationException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -42,12 +45,16 @@ public class GlobalExceptionHandler {
|
||||
* @param e 异常
|
||||
* @return 异常返回
|
||||
*/
|
||||
@IgnoreGlobalReturn
|
||||
@ExceptionHandler(HttpMessageConversionException.class)
|
||||
public TimiResponse<?> conversionException(HttpMessageConversionException e) {
|
||||
public Object conversionException(HttpMessageConversionException e) {
|
||||
log.warn(e.getMessage());
|
||||
if (env.contains("dev") || log.isDebugEnabled()) {
|
||||
log.error("conversion error", e);
|
||||
}
|
||||
if (ignoreCommittedBinaryResponse()) {
|
||||
return null;
|
||||
}
|
||||
return new TimiResponse<>(TimiCode.ARG_BAD).msgKey("invalid.body");
|
||||
}
|
||||
|
||||
@@ -57,12 +64,16 @@ public class GlobalExceptionHandler {
|
||||
* @param e 异常
|
||||
* @return 异常返回
|
||||
*/
|
||||
@IgnoreGlobalReturn
|
||||
@ExceptionHandler(ServletException.class)
|
||||
public TimiResponse<?> headerException(ServletException e) {
|
||||
public Object headerException(ServletException e) {
|
||||
log.warn(e.getMessage());
|
||||
if (env.contains("dev") || log.isDebugEnabled()) {
|
||||
log.error("header error", e);
|
||||
}
|
||||
if (ignoreCommittedBinaryResponse()) {
|
||||
return null;
|
||||
}
|
||||
return new TimiResponse<>(TimiCode.REQUEST_BAD).msgKey("invalid.request");
|
||||
}
|
||||
|
||||
@@ -93,17 +104,46 @@ public class GlobalExceptionHandler {
|
||||
* @param e 异常
|
||||
* @return 异常返回
|
||||
*/
|
||||
@IgnoreGlobalReturn
|
||||
@ExceptionHandler(Throwable.class)
|
||||
public TimiResponse<?> error(Throwable e) {
|
||||
public Object error(Throwable e) {
|
||||
if (e instanceof TimiException timiE) {
|
||||
if (!env.startsWith("prod") || log.isDebugEnabled()) {
|
||||
log.error(timiE.getMessage(), e);
|
||||
}
|
||||
if (ignoreCommittedBinaryResponse()) {
|
||||
return null;
|
||||
}
|
||||
// 一般异常
|
||||
return timiE.toResponse();
|
||||
}
|
||||
// 致命异常
|
||||
log.error("fatal error", e);
|
||||
if (ignoreCommittedBinaryResponse()) {
|
||||
return null;
|
||||
}
|
||||
return new TimiResponse<>(TimiCode.ERROR).msgKey("service.error");
|
||||
}
|
||||
|
||||
/**
|
||||
* 已进入二进制响应写出阶段时,不再尝试输出统一 JSON 包装,
|
||||
* 否则只会制造二次异常并覆盖原始状态码。
|
||||
*
|
||||
* @return `true` 表示当前响应应跳过异常包装
|
||||
*/
|
||||
private boolean ignoreCommittedBinaryResponse() {
|
||||
HttpServletResponse response = TimiSpring.getResponse();
|
||||
if (response == null) {
|
||||
return false;
|
||||
}
|
||||
if (response.isCommitted()) {
|
||||
return true;
|
||||
}
|
||||
String contentType = response.getContentType();
|
||||
if (contentType == null || contentType.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String lowerContentType = contentType.toLowerCase();
|
||||
return !lowerContentType.contains("json") && !lowerContentType.startsWith("text/");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user