Repository navigation
FELIX-6861 Propagate IOExceptions of stream handlers invoked reflectively #575
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,152 @@ | |||||||||||||||||
| /* | |||||||||||||||||
| * Licensed to the Apache Software Foundation (ASF) under one | |||||||||||||||||
| * or more contributor license agreements. See the NOTICE file | |||||||||||||||||
| * distributed with this work for additional information | |||||||||||||||||
| * regarding copyright ownership. The ASF licenses this file | |||||||||||||||||
| * to you under the Apache License, Version 2.0 (the | |||||||||||||||||
| * "License"); you may not use this file except in compliance | |||||||||||||||||
| * with the License. You may obtain a copy of the License at | |||||||||||||||||
| * | |||||||||||||||||
| * http://www.apache.org/licenses/LICENSE-2.0 | |||||||||||||||||
| * | |||||||||||||||||
| * Unless required by applicable law or agreed to in writing, | |||||||||||||||||
| * software distributed under the License is distributed on an | |||||||||||||||||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |||||||||||||||||
| * KIND, either express or implied. See the License for the | |||||||||||||||||
| * specific language governing permissions and limitations | |||||||||||||||||
| * under the License. | |||||||||||||||||
| */ | |||||||||||||||||
| package org.apache.felix.framework; | |||||||||||||||||
|
|
|||||||||||||||||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||||||||||||||||
|
|
|||||||||||||||||
| import java.io.FileNotFoundException; | |||||||||||||||||
| import java.io.IOException; | |||||||||||||||||
| import java.net.Proxy; | |||||||||||||||||
| import java.net.URL; | |||||||||||||||||
| import java.net.URLConnection; | |||||||||||||||||
| import java.net.URLStreamHandler; | |||||||||||||||||
|
|
|||||||||||||||||
| import org.apache.felix.framework.util.SecureAction; | |||||||||||||||||
| import org.junit.jupiter.api.Test; | |||||||||||||||||
| import org.osgi.service.url.AbstractURLStreamHandlerService; | |||||||||||||||||
| import org.osgi.service.url.URLStreamHandlerService; | |||||||||||||||||
|
|
|||||||||||||||||
| /** | |||||||||||||||||
| * An {@link IOException} thrown by a stream handler invoked through reflection is propagated | |||||||||||||||||
| * unchanged (FELIX-6861). | |||||||||||||||||
| */ | |||||||||||||||||
| class URLHandlersStreamHandlerProxyTest | |||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two of the four changed sites have no test.
Both tests construct the proxy with a plain The practical risk is low today, but a later refactor could delete either uncovered
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no such test, and it is worth knowing why before anyone spends time on one: that branch is unreachable as written, so there is nothing to cover. Line 277 looks up a static field on m_action.getDeclaredField(ExtensionManager.class, "m_extensionManager", null)but The name So I would leave the branch out of scope here. Your Both findings from my review are addressed as far as this PR is concerned; no further changes needed from me. |
|||||||||||||||||
| { | |||||||||||||||||
| private static final FileNotFoundException FAILURE = new FileNotFoundException("no such resource"); | |||||||||||||||||
|
|
|||||||||||||||||
| private static final URLStreamHandler FAILING_HANDLER = new URLStreamHandler() | |||||||||||||||||
| { | |||||||||||||||||
| @Override | |||||||||||||||||
| protected URLConnection openConnection(URL url) throws IOException | |||||||||||||||||
| { | |||||||||||||||||
| throw FAILURE; | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| @Override | |||||||||||||||||
| protected URLConnection openConnection(URL url, Proxy proxy) throws IOException | |||||||||||||||||
| { | |||||||||||||||||
| throw FAILURE; | |||||||||||||||||
| } | |||||||||||||||||
| }; | |||||||||||||||||
|
|
|||||||||||||||||
| private static final URLStreamHandler BROKEN_HANDLER = new URLStreamHandler() | |||||||||||||||||
| { | |||||||||||||||||
| @Override | |||||||||||||||||
| protected URLConnection openConnection(URL url) | |||||||||||||||||
| { | |||||||||||||||||
| throw new UnsupportedOperationException("broken"); | |||||||||||||||||
| } | |||||||||||||||||
| }; | |||||||||||||||||
|
|
|||||||||||||||||
| /** | |||||||||||||||||
| * A {@link URLStreamHandlerService} whose {@code openConnection(URL, Proxy)} is public, so that the | |||||||||||||||||
| * proxy invokes it through reflection. | |||||||||||||||||
| */ | |||||||||||||||||
| public static class FailingHandlerService extends AbstractURLStreamHandlerService | |||||||||||||||||
| { | |||||||||||||||||
| @Override | |||||||||||||||||
| public URLConnection openConnection(URL url) throws IOException | |||||||||||||||||
| { | |||||||||||||||||
| throw FAILURE; | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| @Override | |||||||||||||||||
| public URLConnection openConnection(URL url, Proxy proxy) throws IOException | |||||||||||||||||
| { | |||||||||||||||||
| throw FAILURE; | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| /** | |||||||||||||||||
| * A stream handler service that is not a {@link URLStreamHandlerService} of this class loader, as | |||||||||||||||||
| * when it comes from another class loader, so that it is used through a dynamic proxy. | |||||||||||||||||
| */ | |||||||||||||||||
| public static class FailingForeignHandlerService | |||||||||||||||||
| { | |||||||||||||||||
| public URLConnection openConnection(URL url) throws IOException | |||||||||||||||||
| { | |||||||||||||||||
| throw FAILURE; | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| @Test | |||||||||||||||||
| void openConnectionPropagatesIOExceptionOfHandler() throws Exception | |||||||||||||||||
| { | |||||||||||||||||
| URLHandlersStreamHandlerProxy proxy = new URLHandlersStreamHandlerProxy( | |||||||||||||||||
| "felixtest", new SecureAction(), FAILING_HANDLER, null); | |||||||||||||||||
| URL url = new URL(null, "felixtest://host/path", FAILING_HANDLER); | |||||||||||||||||
|
|
|||||||||||||||||
| assertThatThrownBy(() -> proxy.openConnection(url)).isSameAs(FAILURE); | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| @Test | |||||||||||||||||
| void openConnectionWithProxyPropagatesIOExceptionOfHandler() throws Exception | |||||||||||||||||
| { | |||||||||||||||||
| URLHandlersStreamHandlerProxy proxy = new URLHandlersStreamHandlerProxy( | |||||||||||||||||
| "felixtest", new SecureAction(), FAILING_HANDLER, null); | |||||||||||||||||
| URL url = new URL(null, "felixtest://host/path", FAILING_HANDLER); | |||||||||||||||||
|
|
|||||||||||||||||
| assertThatThrownBy(() -> proxy.openConnection(url, Proxy.NO_PROXY)).isSameAs(FAILURE); | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| @Test | |||||||||||||||||
| void openConnectionWithProxyPropagatesIOExceptionOfHandlerService() throws Exception | |||||||||||||||||
| { | |||||||||||||||||
| FailingHandlerService service = new FailingHandlerService(); | |||||||||||||||||
| URLHandlersStreamHandlerProxy proxy = new URLHandlersStreamHandlerProxy( | |||||||||||||||||
| "felixtest", new SecureAction(), service, null); | |||||||||||||||||
| URL url = new URL(null, "felixtest://host/path", FAILING_HANDLER); | |||||||||||||||||
|
|
|||||||||||||||||
| assertThatThrownBy(() -> proxy.openConnection(url, Proxy.NO_PROXY)).isSameAs(FAILURE); | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| @Test | |||||||||||||||||
| void handlerServiceOfAnotherClassLoaderPropagatesIOException() throws Exception | |||||||||||||||||
| { | |||||||||||||||||
| SecureAction action = new SecureAction(); | |||||||||||||||||
| URLStreamHandlerService service = (URLStreamHandlerService) action.createProxy( | |||||||||||||||||
| URLStreamHandlerService.class.getClassLoader(), new Class<?>[]{URLStreamHandlerService.class}, | |||||||||||||||||
| new URLHandlersStreamHandlerProxy(new FailingForeignHandlerService(), action)); | |||||||||||||||||
| URL url = new URL(null, "felixtest://host/path", FAILING_HANDLER); | |||||||||||||||||
|
|
|||||||||||||||||
| assertThatThrownBy(() -> service.openConnection(url)).isSameAs(FAILURE); | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| @Test | |||||||||||||||||
| void openConnectionStillWrapsOtherFailuresOfHandler() throws Exception | |||||||||||||||||
| { | |||||||||||||||||
| URLHandlersStreamHandlerProxy proxy = new URLHandlersStreamHandlerProxy( | |||||||||||||||||
| "felixtest", new SecureAction(), BROKEN_HANDLER, null); | |||||||||||||||||
| URL url = new URL(null, "felixtest://host/path", BROKEN_HANDLER); | |||||||||||||||||
|
|
|||||||||||||||||
| assertThatThrownBy(() -> proxy.openConnection(url)) | |||||||||||||||||
| .isInstanceOf(IllegalStateException.class) | |||||||||||||||||
| .hasRootCauseInstanceOf(UnsupportedOperationException.class); | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
URLStreamHandlerServicefast path still loses theIOException.The four reflective sites are fixed, but
openConnection(URL)returns early at line 266 before reaching any of them:That looks like a direct call, but
svcis frequently a dynamic proxy. When the framework's service object is not an instance of this class loader'sURLStreamHandlerService— the "handler from another class loader" case named in the PR description —getStreamHandlerService()wraps it at line 622:That proxy is
instanceof URLStreamHandlerService, so it takes the line 266 branch, which dispatches intoinvoke()at line 660 —m_action.invokeDirect(...)->Method.invoke. The handler'sIOExceptionbecomes anInvocationTargetException, and sinceITEis not assignable toIOException, the JDK proxy wraps it again inUndeclaredThrowableException.I confirmed the wrapping with a standalone reproduction of that exact shape:
So on this path a
FileNotFoundExceptionstill escapes callers'catch (IOException)blocks as an unchecked exception — the same FELIX-6861 symptom, just with a different wrapper. The call at line 266 isn't inside atryeither, so nothing downgrades it.This is pre-existing rather than introduced here, so I would not hold the PR for it. But since the description frames the fix as covering handlers "from another class loader", it is worth either handling in
invoke()(unwrap theITEthere so the proxy sees a declaredIOException) or calling out explicitly as out of scope so it isn't assumed fixed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, you're right, and the PR description promised exactly that case. Fixed in 2232120:
invoke()now catches theInvocationTargetExceptionand throws its target exception, as a direct call would. The proxy then propagates the declaredIOExceptionofopenConnectioninstead of wrapping it in anUndeclaredThrowableException.handlerServiceOfAnotherClassLoaderPropagatesIOExceptionbuilds the proxy the waygetStreamHandlerService()does, with a service that is not aURLStreamHandlerService. It fails withUndeclaredThrowableExceptionbefore this commit.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verified on
22321201ce— the fix works and both new tests are genuine regression tests rather than ones that would have passed anyway.URLHandlersStreamHandlerProxyTestas it standsinvoke()reverted to04c65f3d3ahandlerServiceOfAnotherClassLoaderPropagatesIOExceptionfails with exactlyUndeclaredThrowableExceptionrethrowIOException(e)at line 340openConnectionWithProxyPropagatesIOExceptionOfHandlerServicefailsdeleteDir/BundleCacheTestfile-locking set I get onmasteron Windows, unchangedThe last row is the one I most wanted to see, since
invoke()is the invocation handler for every proxied method, not justopenConnection. Unwrapping theInvocationTargetExceptionthere also changesparseURLand theURLStreamHandlerSetterpath, and nothing regressed. ThrowinggetTargetException()is also consistent for them_service == nullbranch, which had the same wrapping.Thanks for turning it around so quickly.