优化:区分 HTTP 错误,返回明确原因 (#752)

* 优化:区分 HTTP 错误,返回明确原因

* 修复 缓存关闭共享的 client,会导致后续请求失败

* 修复 缓存关闭共享的 client,会导致后续请求失败

---------

Co-authored-by: 高雄 <admin@cxcp.com>
This commit is contained in:
gaoxingzaq
2026-05-09 15:04:25 +08:00
committed by GitHub
parent 4474ab1d57
commit 1f60e30f09
3 changed files with 113 additions and 23 deletions

View File

@@ -8,6 +8,7 @@ import org.apache.commons.io.FileUtils;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.web.client.HttpClientErrorException;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@@ -46,9 +47,8 @@ public class DownloadUtils {
} }
ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", ""); ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", "");
String realPath = getRelFilePath(fileName, fileAttribute); String realPath = getRelFilePath(fileName, fileAttribute);
// 获取文件后缀用于校验
final String fileSuffix = fileAttribute.getSuffix(); final String fileSuffix = fileAttribute.getSuffix();
// 判断是否非法地址
if (KkFileUtils.isIllegalFileName(realPath)) { if (KkFileUtils.isIllegalFileName(realPath)) {
response.setCode(1); response.setCode(1);
response.setContent(null); response.setContent(null);
@@ -61,17 +61,17 @@ public class DownloadUtils {
response.setMsg("下载失败:不支持的类型!" + urlStr); response.setMsg("下载失败:不支持的类型!" + urlStr);
return response; return response;
} }
if (fileAttribute.isCompressFile()) { //压缩包文件 直接赋予路径 不予下载 if (fileAttribute.isCompressFile()) {
response.setContent(fileDir + fileName); response.setContent(fileDir + fileName);
response.setMsg(fileName); response.setMsg(fileName);
return response; return response;
} }
// 如果文件是否已经存在、且不强制更新,则直接返回文件路径
if (KkFileUtils.isExist(realPath) && !fileAttribute.forceUpdatedCache()) { if (KkFileUtils.isExist(realPath) && !fileAttribute.forceUpdatedCache()) {
response.setContent(realPath); response.setContent(realPath);
response.setMsg(fileName); response.setMsg(fileName);
return response; return response;
} }
try { try {
URL url = WebUtils.normalizedURL(urlStr); URL url = WebUtils.normalizedURL(urlStr);
if (!fileAttribute.getSkipDownLoad()) { if (!fileAttribute.getSkipDownLoad()) {
@@ -79,39 +79,59 @@ public class DownloadUtils {
File realFile = new File(realPath); File realFile = new File(realPath);
CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient(); CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
String finalUrlStr = urlStr; String finalUrlStr = urlStr;
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> {
// 获取响应头中的Content-Type
String contentType = responseWrapper.getContentType();
// 如果是Office/设计文件需要校验MIME类型 final boolean[] hasMimeError = {false};
final String[] mimeErrorMessage = {null};
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> {
String contentType = responseWrapper.getContentType();
if (WebUtils.isMimeCheckRequired(fileSuffix)) { if (WebUtils.isMimeCheckRequired(fileSuffix)) {
if (!WebUtils.isValidMimeType(contentType, fileSuffix)) { if (!WebUtils.isValidMimeType(contentType, fileSuffix)) {
logger.error("文件类型错误期望二进制文件但接收到文本类型url: {}, Content-Type: {}", logger.error("文件类型错误期望二进制文件但接收到文本类型url: {}, Content-Type: {}",
finalUrlStr, contentType); finalUrlStr, contentType);
responseWrapper.setHasError(true); hasMimeError[0] = true;
mimeErrorMessage[0] = "期望二进制文件但接收到文本类型Content-Type: " + contentType;
return; return;
} }
} }
// 保存文件
FileUtils.copyToFile(responseWrapper.getInputStream(), realFile); FileUtils.copyToFile(responseWrapper.getInputStream(), realFile);
}); });
if (hasMimeError[0]) {
response.setCode(1);
response.setContent(null);
response.setMsg(mimeErrorMessage[0]);
return response;
}
} else if (isFtpUrl(url)) { } else if (isFtpUrl(url)) {
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME); String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD); String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD);
String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING); String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING);
String ftpport = WebUtils.getUrlParameterReg(realPath, URL_PARAM_FTP_PORT); String ftpport = WebUtils.getUrlParameterReg(realPath, URL_PARAM_FTP_PORT);
FtpUtils.download(fileAttribute.getUrl(), ftpport, realPath, ftpUsername, ftpPassword, ftpControlEncoding); FtpUtils.download(fileAttribute.getUrl(), ftpport, realPath, ftpUsername, ftpPassword, ftpControlEncoding);
} else if (isFileUrl(url)) { // 添加对file协议的支持 } else if (isFileUrl(url)) {
handleFileProtocol(url, realPath); handleFileProtocol(url, realPath);
} else { } else {
response.setCode(1); response.setCode(1);
response.setMsg("url不能识别url" + urlStr); response.setMsg("url不能识别url" + urlStr);
return response;
} }
} }
response.setContent(realPath); response.setContent(realPath);
response.setMsg(fileName); response.setMsg(fileName);
return response; return response;
} catch (HttpClientErrorException e) {
logger.error("HTTP请求失败状态码{}url{}", e.getStatusCode(), urlStr);
response.setCode(1);
response.setContent(null);
if (e.getStatusCode().is4xxClientError()) {
response.setMsg("文件不存在或无法访问 (HTTP " + e.getStatusCode() + ")");
} else {
response.setMsg("下载失败: " + e.getMessage());
}
return response;
} catch (IOException | GalimatiasParseException e) { } catch (IOException | GalimatiasParseException e) {
logger.error("文件下载失败url{}", urlStr); logger.error("文件下载失败url{}", urlStr);
response.setCode(1); response.setCode(1);
@@ -123,7 +143,11 @@ public class DownloadUtils {
} }
return response; return response;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); logger.error("下载文件时发生未知异常url{}", urlStr, e);
response.setCode(1);
response.setContent(null);
response.setMsg("下载失败: " + e.getMessage());
return response;
} }
} }

View File

@@ -0,0 +1,14 @@
package cn.keking.utils;
import org.springframework.stereotype.Component;
import jakarta.annotation.PreDestroy;
@Component
public class HttpClientLifecycle {
@PreDestroy
public void destroy() {
System.out.println("Spring 容器关闭,释放 HTTP 连接池资源...");
HttpRequestUtils.shutdown();
}
}

View File

@@ -23,6 +23,8 @@ import org.springframework.web.bind.annotation.ResponseBody;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.client.HttpClientErrorException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
@@ -152,34 +154,71 @@ public class OnlinePreviewController {
// 1. 验证接口是否开启 // 1. 验证接口是否开启
if (!ConfigConstants.getGetCorsFile()) { if (!ConfigConstants.getGetCorsFile()) {
logger.info("接口关闭,禁止访问!url{}", urlPath); logger.info("接口关闭,禁止访问!url{}", urlPath);
try {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "接口已关闭");
} catch (IOException ignored) {}
return; return;
} }
// 2. 验证访问权限 // 2. 验证访问权限
if (WebUtils.validateKey(key)) { if (WebUtils.validateKey(key)) {
logger.info("访问不合法:访问密码不正确!url{}", urlPath); logger.info("访问不合法:访问密码不正确!url{}", urlPath);
try {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "访问密码不正确");
} catch (IOException ignored) {}
return; return;
} }
URL url; URL url;
try { try {
urlPath = WebUtils.decodeUrl(urlPath, encryption); urlPath = WebUtils.decodeUrl(urlPath, encryption);
url = WebUtils.normalizedURL(urlPath); url = WebUtils.normalizedURL(urlPath);
} catch (Exception ex) { } catch (Exception ex) {
logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath), ex); logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath), ex);
try {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "URL 解析失败");
} catch (IOException ignored) {}
return; return;
} }
assert urlPath != null; assert urlPath != null;
if (!isHttpUrl(url) && !isFtpUrl(url)) { if (!isHttpUrl(url) && !isFtpUrl(url)) {
logger.info("读取跨域文件异常可能存在非法访问urlPath{}", urlPath); logger.info("读取跨域文件异常可能存在非法访问urlPath{}", urlPath);
try {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "不支持的协议");
} catch (IOException ignored) {}
return; return;
} }
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(urlPath, req);
InputStream inputStream = null;
logger.info("读取跨域文件url{}", urlPath);
if (!isFtpUrl(url)) {
CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(urlPath, req);
logger.info("读取跨域文件url{}", urlPath);
if (!isFtpUrl(url)) {
// HTTP/HTTPS 处理(修复:不关闭共享的 CloseableHttpClient
CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
try {
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> IOUtils.copy(responseWrapper.getInputStream(), response.getOutputStream())); HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> IOUtils.copy(responseWrapper.getInputStream(), response.getOutputStream()));
} catch (HttpClientErrorException e) {
// 捕获 HTTP 4xx 错误(如 404
logger.error("HTTP 请求失败,状态码:{}url{}", e.getStatusCode(), urlPath);
try {
if (e.getStatusCode().is4xxClientError()) {
response.sendError(e.getStatusCode().value(), "文件不存在或无法访问");
} else { } else {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "下载文件时发生错误");
}
} catch (IOException ignored) {
}
} catch (Exception e) {
// 捕获其他异常如连接超时、IO 异常等)
logger.error("读取跨域文件异常url{}", urlPath, e);
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "读取文件失败: " + e.getMessage());
} catch (IOException ignored) {
}
}
} else {
// FTP 处理
InputStream inputStream = null;
try { try {
String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1); String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
String contentType = WebUtils.getContentTypeByFilename(filename); String contentType = WebUtils.getContentTypeByFilename(filename);
@@ -193,7 +232,20 @@ public class OnlinePreviewController {
inputStream = FtpUtils.preview(urlPath, support, urlPath, ftpUsername, ftpPassword, ftpControlEncoding); inputStream = FtpUtils.preview(urlPath, support, urlPath, ftpUsername, ftpPassword, ftpControlEncoding);
IOUtils.copy(inputStream, response.getOutputStream()); IOUtils.copy(inputStream, response.getOutputStream());
} catch (IOException e) { } catch (IOException e) {
logger.error("读取跨域文件异常url{}", urlPath); logger.error("读取跨域文件异常url{}", urlPath, e);
try {
// 根据异常信息判断是否为文件不存在
if (e.getMessage() != null && (e.getMessage().contains("550") || e.getMessage().contains("File not found"))) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "FTP 文件不存在");
} else {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "FTP 读取失败");
}
} catch (IOException ignored) {}
} catch (Exception e) {
logger.error("FTP 预览发生未知异常url{}", urlPath, e);
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "FTP 服务异常");
} catch (IOException ignored) {}
} finally { } finally {
IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(inputStream);
} }