v0.0.16
CI/CD / build-deploy (pull_request) Successful in 1m51s

This commit is contained in:
Timi
2026-07-31 15:11:38 +08:00
parent c2cb8771ce
commit 8736aa44c4
5 changed files with 252 additions and 13 deletions
@@ -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/");
}
}