From ee60eb7207c6867ea7e70464ed8321a5cf35306c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 20 Jan 2025 18:17:49 +0100 Subject: [PATCH] Fall back to HTTP GET in case of 405 from HTTP HEAD Closes gh-34217 --- .../io/AbstractFileResolvingResource.java | 48 +++++++++++++-- .../core/io/ResourceTests.java | 59 +++++++++++++++---- 2 files changed, 90 insertions(+), 17 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java index dba00a04518..bd35c7e166c 100644 --- a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,6 +66,20 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { else if (code == HttpURLConnection.HTTP_NOT_FOUND) { return false; } + else if (code == HttpURLConnection.HTTP_BAD_METHOD) { + con = url.openConnection(); + customizeConnection(con); + if (con instanceof HttpURLConnection newHttpCon) { + code = newHttpCon.getResponseCode(); + if (code == HttpURLConnection.HTTP_OK) { + return true; + } + else if (code == HttpURLConnection.HTTP_NOT_FOUND) { + return false; + } + httpCon = newHttpCon; + } + } } if (con.getContentLengthLong() > 0) { return true; @@ -111,6 +125,15 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { if (con instanceof HttpURLConnection httpCon) { httpCon.setRequestMethod("HEAD"); int code = httpCon.getResponseCode(); + if (code == HttpURLConnection.HTTP_BAD_METHOD) { + con = url.openConnection(); + customizeConnection(con); + if (!(con instanceof HttpURLConnection newHttpCon)) { + return false; + } + code = newHttpCon.getResponseCode(); + httpCon = newHttpCon; + } if (code != HttpURLConnection.HTTP_OK) { httpCon.disconnect(); return false; @@ -259,7 +282,14 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { if (con instanceof HttpURLConnection httpCon) { httpCon.setRequestMethod("HEAD"); } - return con.getContentLengthLong(); + long length = con.getContentLengthLong(); + if (length <= 0 && con instanceof HttpURLConnection httpCon && + httpCon.getResponseCode() == HttpURLConnection.HTTP_BAD_METHOD) { + con = url.openConnection(); + customizeConnection(con); + length = con.getContentLengthLong(); + } + return length; } } @@ -288,9 +318,17 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { httpCon.setRequestMethod("HEAD"); } long lastModified = con.getLastModified(); - if (fileCheck && lastModified == 0 && con.getContentLengthLong() <= 0) { - throw new FileNotFoundException(getDescription() + - " cannot be resolved in the file system for checking its last-modified timestamp"); + if (lastModified == 0) { + if (con instanceof HttpURLConnection httpCon && + httpCon.getResponseCode() == HttpURLConnection.HTTP_BAD_METHOD) { + con = url.openConnection(); + customizeConnection(con); + lastModified = con.getLastModified(); + } + if (fileCheck && con.getContentLengthLong() <= 0) { + throw new FileNotFoundException(getDescription() + + " cannot be resolved in the file system for checking its last-modified timestamp"); + } } return lastModified; } diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java index 5e62e3c05f7..5d1b19123c8 100644 --- a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -306,7 +306,9 @@ class ResourceTests { @Nested class UrlResourceTests { - private MockWebServer server = new MockWebServer(); + private static final String LAST_MODIFIED = "Wed, 09 Apr 2014 09:57:42 GMT"; + + private final MockWebServer server = new MockWebServer(); @Test void sameResourceWithRelativePathIsEqual() throws Exception { @@ -385,22 +387,44 @@ class ResourceTests { @Test void missingRemoteResourceDoesNotExist() throws Exception { - String baseUrl = startServer(); + String baseUrl = startServer(true); UrlResource resource = new UrlResource(baseUrl + "/missing"); assertThat(resource.exists()).isFalse(); } @Test void remoteResourceExists() throws Exception { - String baseUrl = startServer(); + String baseUrl = startServer(true); + UrlResource resource = new UrlResource(baseUrl + "/resource"); + assertThat(resource.exists()).isTrue(); + assertThat(resource.isReadable()).isTrue(); + assertThat(resource.contentLength()).isEqualTo(6); + assertThat(resource.lastModified()).isGreaterThan(0); + } + + @Test + void remoteResourceExistsFallback() throws Exception { + String baseUrl = startServer(false); UrlResource resource = new UrlResource(baseUrl + "/resource"); assertThat(resource.exists()).isTrue(); + assertThat(resource.isReadable()).isTrue(); assertThat(resource.contentLength()).isEqualTo(6); + assertThat(resource.lastModified()).isGreaterThan(0); } @Test void canCustomizeHttpUrlConnectionForExists() throws Exception { - String baseUrl = startServer(); + String baseUrl = startServer(true); + CustomResource resource = new CustomResource(baseUrl + "/resource"); + assertThat(resource.exists()).isTrue(); + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("HEAD"); + assertThat(request.getHeader("Framework-Name")).isEqualTo("Spring"); + } + + @Test + void canCustomizeHttpUrlConnectionForExistsFallback() throws Exception { + String baseUrl = startServer(false); CustomResource resource = new CustomResource(baseUrl + "/resource"); assertThat(resource.exists()).isTrue(); RecordedRequest request = this.server.takeRequest(); @@ -410,7 +434,7 @@ class ResourceTests { @Test void canCustomizeHttpUrlConnectionForRead() throws Exception { - String baseUrl = startServer(); + String baseUrl = startServer(true); CustomResource resource = new CustomResource(baseUrl + "/resource"); assertThat(resource.getInputStream()).hasContent("Spring"); RecordedRequest request = this.server.takeRequest(); @@ -420,7 +444,7 @@ class ResourceTests { @Test void useUserInfoToSetBasicAuth() throws Exception { - startServer(); + startServer(true); UrlResource resource = new UrlResource( "http://alice:secret@localhost:" + this.server.getPort() + "/resource"); assertThat(resource.getInputStream()).hasContent("Spring"); @@ -436,8 +460,8 @@ class ResourceTests { this.server.shutdown(); } - private String startServer() throws Exception { - this.server.setDispatcher(new ResourceDispatcher()); + private String startServer(boolean withHeadSupport) throws Exception { + this.server.setDispatcher(new ResourceDispatcher(withHeadSupport)); this.server.start(); return "http://localhost:" + this.server.getPort(); } @@ -456,15 +480,26 @@ class ResourceTests { class ResourceDispatcher extends Dispatcher { + boolean withHeadSupport; + + public ResourceDispatcher(boolean withHeadSupport) { + this.withHeadSupport = withHeadSupport; + } + @Override public MockResponse dispatch(RecordedRequest request) { if (request.getPath().equals("/resource")) { return switch (request.getMethod()) { - case "HEAD" -> new MockResponse() - .addHeader("Content-Length", "6"); + case "HEAD" -> (this.withHeadSupport ? + new MockResponse() + .addHeader("Content-Type", "text/plain") + .addHeader("Content-Length", "6") + .addHeader("Last-Modified", LAST_MODIFIED) : + new MockResponse().setResponseCode(405)); case "GET" -> new MockResponse() - .addHeader("Content-Length", "6") .addHeader("Content-Type", "text/plain") + .addHeader("Content-Length", "6") + .addHeader("Last-Modified", LAST_MODIFIED) .setBody("Spring"); default -> new MockResponse().setResponseCode(404); };