• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20291

22 May 2026 06:37PM UTC coverage: 88.86% (+0.02%) from 88.837%
#20291

push

github

web-flow
core: throw IOException when ProxySelector returns null or empty list (#12793)

ProxySelector.select(URI) is contractually required to return a non-null, non-empty list. Some implementations violate this, which previously caused an opaque crash in ProxyDetectorImpl:

```
java.lang.IndexOutOfBoundsException: Index: 0
    at java.util.Collections$EmptyList.get
    at io.grpc.internal.ProxyDetectorImpl.detectProxy
```

Detect this case explicitly and throw an IOException naming the offending ProxySelector class, so a broken implementation can be identified and fixed by its author rather than silently worked around in every caller.

36286 of 40835 relevant lines covered (88.86%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.87
/../core/src/main/java/io/grpc/internal/ProxyDetectorImpl.java
1
/*
2
 * Copyright 2017 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.internal;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20

21
import com.google.common.annotations.VisibleForTesting;
22
import com.google.common.base.Supplier;
23
import io.grpc.HttpConnectProxiedSocketAddress;
24
import io.grpc.ProxiedSocketAddress;
25
import io.grpc.ProxyDetector;
26
import java.io.IOException;
27
import java.net.Authenticator;
28
import java.net.InetAddress;
29
import java.net.InetSocketAddress;
30
import java.net.MalformedURLException;
31
import java.net.PasswordAuthentication;
32
import java.net.Proxy;
33
import java.net.ProxySelector;
34
import java.net.SocketAddress;
35
import java.net.URI;
36
import java.net.URISyntaxException;
37
import java.net.URL;
38
import java.util.List;
39
import java.util.logging.Level;
40
import java.util.logging.Logger;
41
import javax.annotation.Nullable;
42

43
/**
44
 * A utility class that detects proxies using {@link ProxySelector} and detects authentication
45
 * credentials using {@link Authenticator}.
46
 *
47
 */
48
class ProxyDetectorImpl implements ProxyDetector {
49
  // To validate this code: set up a local squid proxy instance, and
50
  // try to communicate with grpc-test.sandbox.googleapis.com:443.
51
  // The endpoint runs an instance of TestServiceGrpc, see
52
  // AbstractInteropTest for an example how to run a
53
  // TestService.EmptyCall RPC.
54
  //
55
  // The instructions below assume Squid 3.5.23 and a recent
56
  // version of Debian.
57
  //
58
  // Set the contents of /etc/squid/squid.conf to be:
59
  // WARNING: THESE CONFIGS HAVE NOT BEEN REVIEWED FOR SECURITY, DO
60
  // NOT USE OUTSIDE OF TESTING. COMMENT OUT THIS WARNING TO
61
  // UNBREAK THE CONFIG FILE.
62
  // acl SSL_ports port 443
63
  // acl Safe_ports port 80
64
  // acl Safe_ports port 21
65
  // acl Safe_ports port 443
66
  // acl Safe_ports port 70
67
  // acl Safe_ports port 210
68
  // acl Safe_ports port 1025-65535
69
  // acl Safe_ports port 280
70
  // acl Safe_ports port 488
71
  // acl Safe_ports port 591
72
  // acl Safe_ports port 777
73
  // acl CONNECT method CONNECT
74
  // http_access deny !Safe_ports
75
  // http_access deny CONNECT !SSL_ports
76
  // http_access allow localhost manager
77
  // http_access deny manager
78
  // http_access allow localhost
79
  // http_access deny all
80
  // http_port 3128
81
  // coredump_dir /var/spool/squid
82
  // refresh_pattern ^ftp: 1440 20% 10080
83
  // refresh_pattern ^gopher: 1440 0% 1440
84
  // refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
85
  // refresh_pattern . 0 20% 4320
86
  //
87
  // Restart squid:
88
  // $ sudo /etc/init.d/squid restart
89
  //
90
  // To test with passwords:
91
  //
92
  // Run this command and follow the instructions to set up a user/pass:
93
  // $ sudo htpasswd -c /etc/squid/passwd myuser1
94
  //
95
  // Make the file readable to squid:
96
  // $ sudo chmod 644 /etc/squid/passwd
97
  //
98
  // Validate the username and password, you should see OK printed:
99
  // $ /usr/lib/squid3/basic_ncsa_auth /etc/squid/passwd
100
  // myuser1 <your password here>
101
  //
102
  // Add these additional lines to the beginning of squid.conf (the ordering matters):
103
  // auth_param basic program /usr/lib/squid3/basic_ncsa_auth /etc/squid/passwd
104
  // auth_param basic children 5
105
  // auth_param basic realm Squid proxy-caching web server
106
  // auth_param basic credentialsttl 2 hours
107
  // acl ncsa_users proxy_auth REQUIRED
108
  // http_access allow ncsa_users
109
  //
110
  // Restart squid:
111
  // $ sudo /etc/init.d/squid restart
112
  //
113
  // In both cases, start the JVM with -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=3128 to
114
  // configure the proxy. For passwords, use java.net.Authenticator.setDefault().
115
  //
116
  // Testing with curl, no password:
117
  // $ curl -U myuser1:pass1 -x http://localhost:3128 -L grpc.io
118
  // Testing with curl, with password:
119
  // $ curl -U myuser1:pass1 -x http://localhost:3128 -L grpc.io
120
  //
121
  // It may be helpful to monitor the squid access logs:
122
  // $ sudo tail -f /var/log/squid/access.log
123

124
  private static final Logger log = Logger.getLogger(ProxyDetectorImpl.class.getName());
1✔
125
  private static final AuthenticationProvider DEFAULT_AUTHENTICATOR = new AuthenticationProvider() {
1✔
126
    @Override
127
    public PasswordAuthentication requestPasswordAuthentication(
128
        String host, InetAddress addr, int port, String protocol, String prompt, String scheme) {
129
      URL url = null;
×
130
      try {
131
        url = new URL(protocol, host, port, "");
×
132
      } catch (MalformedURLException e) {
×
133
        // let url be null
134
        log.log(
×
135
            Level.WARNING,
136
            "failed to create URL for Authenticator: {0} {1}", new Object[] {protocol, host});
137
      }
×
138
      return Authenticator.requestPasswordAuthentication(
×
139
          host, addr, port, protocol, prompt, scheme, url, Authenticator.RequestorType.PROXY);
140
    }
141
  };
142
  private static final Supplier<ProxySelector> DEFAULT_PROXY_SELECTOR =
1✔
143
      new Supplier<ProxySelector>() {
1✔
144
        @Override
145
        public ProxySelector get() {
146
          return ProxySelector.getDefault();
1✔
147
        }
148
      };
149

150
  // Do not hard code a ProxySelector because the global default ProxySelector can change
151
  private final Supplier<ProxySelector> proxySelector;
152
  private final AuthenticationProvider authenticationProvider;
153

154
  // We want an HTTPS proxy, which operates on the entire data stream (See IETF rfc2817).
155
  static final String PROXY_SCHEME = "https";
156

157
  /**
158
   * A proxy selector that uses the global {@link ProxySelector#getDefault()} and
159
   * {@link ProxyDetectorImpl.AuthenticationProvider} to detect proxy parameters.
160
   */
161
  public ProxyDetectorImpl() {
162
    this(DEFAULT_PROXY_SELECTOR, DEFAULT_AUTHENTICATOR);
1✔
163
  }
1✔
164

165
  @VisibleForTesting
166
  ProxyDetectorImpl(
167
      Supplier<ProxySelector> proxySelector,
168
      AuthenticationProvider authenticationProvider) {
1✔
169
    this.proxySelector = checkNotNull(proxySelector);
1✔
170
    this.authenticationProvider = checkNotNull(authenticationProvider);
1✔
171
  }
1✔
172

173
  @Nullable
174
  @Override
175
  public ProxiedSocketAddress proxyFor(SocketAddress targetServerAddress) throws IOException {
176
    if (!(targetServerAddress instanceof InetSocketAddress)) {
1✔
177
      return null;
1✔
178
    }
179
    return detectProxy((InetSocketAddress) targetServerAddress);
1✔
180
  }
181

182
  private ProxiedSocketAddress detectProxy(InetSocketAddress targetAddr) throws IOException {
183
    URI uri;
184
    String host = targetAddr.getHostString();
1✔
185
    try {
186
      uri =
1✔
187
          new URI(
188
              PROXY_SCHEME,
189
              null, /* userInfo */
190
              host,
191
              targetAddr.getPort(),
1✔
192
              null, /* path */
193
              null, /* query */
194
              null /* fragment */);
195
    } catch (final URISyntaxException e) {
×
196
      log.log(
×
197
          Level.WARNING,
198
          "Failed to construct URI for proxy lookup, proceeding without proxy",
199
          e);
200
      return null;
×
201
    }
1✔
202

203
    ProxySelector proxySelector = this.proxySelector.get();
1✔
204
    if (proxySelector == null) {
1✔
205
      log.log(Level.FINE, "proxy selector is null, so continuing without proxy lookup");
1✔
206
      return null;
1✔
207
    }
208

209
    List<Proxy> proxies = proxySelector.select(uri);
1✔
210
    // ProxySelector.select(URI) is contractually required to return a non-null, non-empty list.
211
    // Surface the offending implementation's class name so a broken ProxySelector can be fixed.
212
    if (proxies == null || proxies.isEmpty()) {
1✔
213
      throw new IOException(
1✔
214
          "ProxySelector " + proxySelector.getClass().getName()
1✔
215
              + " returned " + (proxies == null ? "null" : "an empty list")
1✔
216
              + ", which violates the java.net.ProxySelector#select(URI) contract");
217
    }
218
    if (proxies.size() > 1) {
1✔
219
      log.warning("More than 1 proxy detected, gRPC will select the first one");
1✔
220
    }
221
    Proxy proxy = proxies.get(0);
1✔
222

223
    if (proxy.type() == Proxy.Type.DIRECT) {
1✔
224
      return null;
1✔
225
    }
226
    InetSocketAddress proxyAddr = (InetSocketAddress) proxy.address();
1✔
227
    // The prompt string should be the realm as returned by the server.
228
    // We don't have it because we are avoiding the full handshake.
229
    String promptString = "";
1✔
230
    PasswordAuthentication auth =
1✔
231
        authenticationProvider.requestPasswordAuthentication(
1✔
232
            proxyAddr.getHostString(),
1✔
233
            proxyAddr.getAddress(),
1✔
234
            proxyAddr.getPort(),
1✔
235
            PROXY_SCHEME,
236
            promptString,
237
            null);
238

239
    final InetSocketAddress resolvedProxyAddr;
240
    if (proxyAddr.isUnresolved()) {
1✔
241
      InetAddress resolvedAddress = InetAddress.getByName(proxyAddr.getHostName());
1✔
242
      resolvedProxyAddr = new InetSocketAddress(resolvedAddress, proxyAddr.getPort());
1✔
243
    } else {
1✔
244
      resolvedProxyAddr = proxyAddr;
×
245
    }
246

247
    HttpConnectProxiedSocketAddress.Builder builder =
248
        HttpConnectProxiedSocketAddress.newBuilder()
1✔
249
        .setTargetAddress(targetAddr)
1✔
250
        .setProxyAddress(resolvedProxyAddr);
1✔
251

252
    if (auth == null) {
1✔
253
      return builder.build();
1✔
254
    }
255

256
    return builder
1✔
257
        .setUsername(auth.getUserName())
1✔
258
        .setPassword(auth.getPassword() == null ? null : new String(auth.getPassword()))
1✔
259
        .build();
1✔
260
  }
261

262
  /**
263
   * This interface makes unit testing easier by avoiding direct calls to static methods.
264
   */
265
  interface AuthenticationProvider {
266
    PasswordAuthentication requestPasswordAuthentication(
267
        String host,
268
        InetAddress addr,
269
        int port,
270
        String protocol,
271
        String prompt,
272
        String scheme);
273
  }
274
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc