xref: /aosp_15_r20/external/jsoup/src/test/java/org/jsoup/integration/UrlConnectTest.java (revision 6da8f8c4bc310ad659121b84dd089062417a2ce2)
1 package org.jsoup.integration;
2 
3 import org.jsoup.Connection;
4 import org.jsoup.Jsoup;
5 import org.jsoup.UnsupportedMimeTypeException;
6 import org.jsoup.helper.W3CDom;
7 import org.jsoup.internal.StringUtil;
8 import org.jsoup.nodes.Document;
9 import org.jsoup.nodes.FormElement;
10 import org.jsoup.parser.HtmlTreeBuilder;
11 import org.jsoup.parser.Parser;
12 import org.jsoup.parser.XmlTreeBuilder;
13 import org.junit.jupiter.api.Disabled;
14 import org.junit.jupiter.api.Test;
15 
16 import java.io.File;
17 import java.io.FileInputStream;
18 import java.io.IOException;
19 import java.net.ConnectException;
20 import java.net.InetSocketAddress;
21 import java.net.Proxy;
22 import java.net.URL;
23 import java.util.List;
24 
25 import static org.junit.jupiter.api.Assertions.*;
26 
27 /**
28  Tests the URL connection. Not enabled by default, so tests don't require network connection.
29 
30  @author Jonathan Hedley, [email protected] */
31 @Disabled // ignored by default so tests don't require network access. comment out to enable.
32 // todo: rebuild these into a local Jetty test server, so not reliant on the vagaries of the internet.
33 public class UrlConnectTest {
34     private static final String WEBSITE_WITH_INVALID_CERTIFICATE = "https://certs.cac.washington.edu/CAtest/";
35     private static final String WEBSITE_WITH_SNI = "https://jsoup.org/";
36     public static String browserUa = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36";
37 
38     @Test
fetchBaidu()39     public void fetchBaidu() throws IOException {
40         Connection.Response res = Jsoup.connect("http://www.baidu.com/").timeout(10*1000).execute();
41         Document doc = res.parse();
42 
43         assertEquals("GBK", doc.outputSettings().charset().displayName());
44         assertEquals("GBK", res.charset());
45         assert(res.hasCookie("BAIDUID"));
46         assertEquals("text/html;charset=gbk", res.contentType());
47     }
48 
49     @Test
exceptOnUnknownContentType()50     public void exceptOnUnknownContentType() {
51         String url = "http://direct.jsoup.org/rez/osi_logo.png"; // not text/* but image/png, should throw
52         boolean threw = false;
53         try {
54             Document doc = Jsoup.parse(new URL(url), 3000);
55         } catch (UnsupportedMimeTypeException e) {
56             threw = true;
57             assertEquals("org.jsoup.UnsupportedMimeTypeException: Unhandled content type. Must be text/*, application/xml, or application/xhtml+xml. Mimetype=image/png, URL=http://direct.jsoup.org/rez/osi_logo.png", e.toString());
58             assertEquals(url, e.getUrl());
59             assertEquals("image/png", e.getMimeType());
60         } catch (IOException e) {
61         }
62         assertTrue(threw);
63     }
64 
65     @Test
ignoresContentTypeIfSoConfigured()66     public void ignoresContentTypeIfSoConfigured() throws IOException {
67         Document doc = Jsoup.connect("https://jsoup.org/rez/osi_logo.png").ignoreContentType(true).get();
68         assertEquals("", doc.title()); // this will cause an ugly parse tree
69     }
70 
ihVal(String key, Document doc)71     private static String ihVal(String key, Document doc) {
72         return doc.select("th:contains("+key+") + td").first().text();
73     }
74 
75     @Test
followsTempRedirect()76     public void followsTempRedirect() throws IOException {
77         Connection con = Jsoup.connect("http://direct.infohound.net/tools/302.pl"); // http://jsoup.org
78         Document doc = con.get();
79         assertTrue(doc.title().contains("jsoup"));
80     }
81 
82     @Test
followsNewTempRedirect()83     public void followsNewTempRedirect() throws IOException {
84         Connection con = Jsoup.connect("http://direct.infohound.net/tools/307.pl"); // http://jsoup.org
85         Document doc = con.get();
86         assertTrue(doc.title().contains("jsoup"));
87         assertEquals("https://jsoup.org/", con.response().url().toString());
88     }
89 
90     @Test
postRedirectsFetchWithGet()91     public void postRedirectsFetchWithGet() throws IOException {
92         Connection con = Jsoup.connect("http://direct.infohound.net/tools/302.pl")
93                 .data("Argument", "Riposte")
94                 .method(Connection.Method.POST);
95         Connection.Response res = con.execute();
96         assertEquals("https://jsoup.org/", res.url().toExternalForm());
97         assertEquals(Connection.Method.GET, res.method());
98     }
99 
100     @Test
followsRedirectToHttps()101     public void followsRedirectToHttps() throws IOException {
102         Connection con = Jsoup.connect("http://direct.infohound.net/tools/302-secure.pl"); // https://www.google.com
103         con.data("id", "5");
104         Document doc = con.get();
105         assertTrue(doc.title().contains("Google"));
106     }
107 
108     @Test
followsRelativeRedirect()109     public void followsRelativeRedirect() throws IOException {
110         Connection con = Jsoup.connect("http://direct.infohound.net/tools/302-rel.pl"); // to /tidy/
111         Document doc = con.post();
112         assertTrue(doc.title().contains("HTML Tidy Online"));
113     }
114 
115     @Test
followsRelativeDotRedirect()116     public void followsRelativeDotRedirect() throws IOException {
117         // redirects to "./ok.html", should resolve to http://direct.infohound.net/tools/ok.html
118         Connection con = Jsoup.connect("http://direct.infohound.net/tools/302-rel-dot.pl"); // to ./ok.html
119         Document doc = con.post();
120         assertTrue(doc.title().contains("OK"));
121         assertEquals(doc.location(), "http://direct.infohound.net/tools/ok.html");
122     }
123 
124     @Test
followsRelativeDotRedirect2()125     public void followsRelativeDotRedirect2() throws IOException {
126         //redirects to "esportspenedes.cat/./ep/index.php", should resolve to "esportspenedes.cat/ep/index.php"
127         Connection con = Jsoup.connect("http://esportspenedes.cat")  // note lack of trailing / - server should redir to / first, then to ./ep/...; but doesn't'
128                 .timeout(10000);
129         Document doc = con.post();
130         assertEquals(doc.location(), "http://esportspenedes.cat/ep/index.php");
131     }
132 
133     @Test
followsRedirectsWithWithespaces()134     public void followsRedirectsWithWithespaces() throws IOException {
135         Connection con = Jsoup.connect("http://tinyurl.com/kgofxl8"); // to http://www.google.com/?q=white spaces
136         Document doc = con.get();
137         assertTrue(doc.title().contains("Google"));
138     }
139 
140     @Test
gracefullyHandleBrokenLocationRedirect()141     public void gracefullyHandleBrokenLocationRedirect() throws IOException {
142         Connection con = Jsoup.connect("http://aag-ye.com"); // has Location: http:/temp/AAG_New/en/index.php
143         con.get(); // would throw exception on error
144         assertTrue(true);
145     }
146 
147     @Test
ignores500tExceptionIfSoConfigured()148     public void ignores500tExceptionIfSoConfigured() throws IOException {
149         Connection con = Jsoup.connect("http://direct.infohound.net/tools/500.pl").ignoreHttpErrors(true);
150         Connection.Response res = con.execute();
151         Document doc = res.parse();
152         assertEquals(500, res.statusCode());
153         assertEquals("Application Error", res.statusMessage());
154         assertEquals("Woops", doc.select("h1").first().text());
155     }
156 
157     @Test
ignores500WithNoContentExceptionIfSoConfigured()158     public void ignores500WithNoContentExceptionIfSoConfigured() throws IOException {
159         Connection con = Jsoup.connect("http://direct.infohound.net/tools/500-no-content.pl").ignoreHttpErrors(true);
160         Connection.Response res = con.execute();
161         Document doc = res.parse();
162         assertEquals(500, res.statusCode());
163         assertEquals("Application Error", res.statusMessage());
164     }
165 
166     @Test
ignores200WithNoContentExceptionIfSoConfigured()167     public void ignores200WithNoContentExceptionIfSoConfigured() throws IOException {
168         Connection con = Jsoup.connect("http://direct.infohound.net/tools/200-no-content.pl").ignoreHttpErrors(true);
169         Connection.Response res = con.execute();
170         Document doc = res.parse();
171         assertEquals(200, res.statusCode());
172         assertEquals("All Good", res.statusMessage());
173     }
174 
175     @Test
handles200WithNoContent()176     public void handles200WithNoContent() throws IOException {
177         Connection con = Jsoup
178             .connect("http://direct.infohound.net/tools/200-no-content.pl")
179             .userAgent(browserUa);
180         Connection.Response res = con.execute();
181         Document doc = res.parse();
182         assertEquals(200, res.statusCode());
183 
184         con = Jsoup
185             .connect("http://direct.infohound.net/tools/200-no-content.pl")
186             .parser(Parser.xmlParser())
187             .userAgent(browserUa);
188         res = con.execute();
189         doc = res.parse();
190         assertEquals(200, res.statusCode());
191     }
192 
193     @Test
doesntRedirectIfSoConfigured()194     public void doesntRedirectIfSoConfigured() throws IOException {
195         Connection con = Jsoup.connect("http://direct.infohound.net/tools/302.pl").followRedirects(false);
196         Connection.Response res = con.execute();
197         assertEquals(302, res.statusCode());
198         assertEquals("http://jsoup.org", res.header("Location"));
199     }
200 
201     @Test
redirectsResponseCookieToNextResponse()202     public void redirectsResponseCookieToNextResponse() throws IOException {
203         Connection con = Jsoup.connect("http://direct.infohound.net/tools/302-cookie.pl");
204         Connection.Response res = con.execute();
205         assertEquals("asdfg123", res.cookie("token")); // confirms that cookies set on 1st hit are presented in final result
206         Document doc = res.parse();
207         assertEquals("token=asdfg123; uid=jhy", ihVal("HTTP_COOKIE", doc)); // confirms that redirected hit saw cookie
208     }
209 
210     @Test
maximumRedirects()211     public void maximumRedirects() {
212         boolean threw = false;
213         try {
214             Document doc = Jsoup.connect("http://direct.infohound.net/tools/loop.pl").get();
215         } catch (IOException e) {
216             assertTrue(e.getMessage().contains("Too many redirects"));
217             threw = true;
218         }
219         assertTrue(threw);
220     }
221 
222     @Test
handlesDodgyCharset()223     public void handlesDodgyCharset() throws IOException {
224         // tests that when we get back "UFT8", that it is recognised as unsupported, and falls back to default instead
225         String url = "http://direct.infohound.net/tools/bad-charset.pl";
226         Connection.Response res = Jsoup.connect(url).execute();
227         assertEquals("text/html; charset=UFT8", res.header("Content-Type")); // from the header
228         assertNull(res.charset()); // tried to get from header, not supported, so returns null
229         Document doc = res.parse(); // would throw an error if charset unsupported
230         assertTrue(doc.text().contains("Hello!"));
231         assertEquals("UTF-8", res.charset()); // set from default on parse
232     }
233 
234     /**
235      * Verify that security disabling feature works properly.
236      * <p/>
237      * 1. try to hit url with invalid certificate and evaluate that exception is thrown
238      *
239      * @throws Exception
240      */
241     @Test
testUnsafeFail()242     public void testUnsafeFail() throws Exception {
243         String url = WEBSITE_WITH_INVALID_CERTIFICATE;
244         assertThrows(IOException.class, () -> Jsoup.connect(url).execute());
245     }
246 
247 
248     /**
249      * Verify that requests to websites with SNI fail on jdk 1.6
250      * <p/>
251      * read for more details:
252      * http://en.wikipedia.org/wiki/Server_Name_Indication
253      *
254      * Test is ignored independent from others as it requires JDK 1.6
255      * @throws Exception
256      */
257     @Test
testSNIFail()258     public void testSNIFail() throws Exception {
259         assertThrows(IOException.class, () -> Jsoup.connect(WEBSITE_WITH_SNI).execute());
260     }
261 
262     @Test
shouldWorkForCharsetInExtraAttribute()263     public void shouldWorkForCharsetInExtraAttribute() throws IOException {
264         Connection.Response res = Jsoup.connect("https://www.creditmutuel.com/groupe/fr/").execute();
265         Document doc = res.parse(); // would throw an error if charset unsupported
266         assertEquals("ISO-8859-1", res.charset());
267     }
268 
269     // The following tests were added to test specific domains if they work. All code paths
270     // which make the following test green are tested in other unit or integration tests, so the following lines
271     // could be deleted
272 
273     @Test
shouldSelectFirstCharsetOnWeirdMultileCharsetsInMetaTags()274     public void shouldSelectFirstCharsetOnWeirdMultileCharsetsInMetaTags() throws IOException {
275         Connection.Response res = Jsoup.connect("http://aamo.info/").execute();
276         res.parse(); // would throw an error if charset unsupported
277         assertEquals("ISO-8859-1", res.charset());
278     }
279 
280     @Test
shouldParseBrokenHtml5MetaCharsetTagCorrectly()281     public void shouldParseBrokenHtml5MetaCharsetTagCorrectly() throws IOException {
282         Connection.Response res = Jsoup.connect("http://9kuhkep.net").execute();
283         res.parse(); // would throw an error if charset unsupported
284         assertEquals("UTF-8", res.charset());
285     }
286 
287     @Test
shouldEmptyMetaCharsetCorrectly()288     public void shouldEmptyMetaCharsetCorrectly() throws IOException {
289         Connection.Response res = Jsoup.connect("http://aastmultimedia.com").execute();
290         res.parse(); // would throw an error if charset unsupported
291         assertEquals("UTF-8", res.charset());
292     }
293 
294     @Test
shouldWorkForDuplicateCharsetInTag()295     public void shouldWorkForDuplicateCharsetInTag() throws IOException {
296         Connection.Response res = Jsoup.connect("http://aaptsdassn.org").execute();
297         Document doc = res.parse(); // would throw an error if charset unsupported
298         assertEquals("ISO-8859-1", res.charset());
299     }
300 
301     @Test
handles201Created()302     public void handles201Created() throws IOException {
303         Document doc = Jsoup.connect("http://direct.infohound.net/tools/201.pl").get(); // 201, location=jsoup
304         assertEquals("https://jsoup.org/", doc.location());
305     }
306 
307     /*
308      Proxy tests. Assumes local proxy running on 8888, without system propery set (so that specifying it is required).
309      */
310 
311     @Test
fetchViaHttpProxy()312     public void fetchViaHttpProxy() throws IOException {
313         String url = "https://jsoup.org";
314         Proxy proxy = new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved("localhost", 8888));
315         Document doc = Jsoup.connect(url).proxy(proxy).get();
316         assertTrue(doc.title().contains("jsoup"));
317     }
318 
319     @Test
fetchViaHttpProxySetByArgument()320     public void fetchViaHttpProxySetByArgument() throws IOException {
321         String url = "https://jsoup.org";
322         Document doc = Jsoup.connect(url).proxy("localhost", 8888).get();
323         assertTrue(doc.title().contains("jsoup"));
324     }
325 
326     @Test
invalidProxyFails()327     public void invalidProxyFails() {
328         boolean caught = false;
329         String url = "https://jsoup.org";
330         try {
331             Document doc = Jsoup.connect(url).proxy("localhost", 8889).get();
332         } catch (IOException e) {
333             caught = e instanceof ConnectException;
334         }
335         assertTrue(caught);
336     }
337 
338     @Test
proxyGetAndSet()339     public void proxyGetAndSet() throws IOException {
340         String url = "https://jsoup.org";
341         Proxy proxy = new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved("localhost", 8889)); // invalid
342         final Connection con = Jsoup.connect(url).proxy(proxy);
343 
344         assert con.request().proxy() == proxy;
345         con.request().proxy(null); // disable
346         Document doc = con.get();
347         assertTrue(doc.title().contains("jsoup")); // would fail if actually went via proxy
348     }
349 
350     @Test
throwsIfRequestBodyForGet()351     public void throwsIfRequestBodyForGet() throws IOException {
352         boolean caught = false;
353         String url = "https://jsoup.org";
354         try {
355             Document doc = Jsoup.connect(url).requestBody("fail").get();
356         } catch (IllegalArgumentException e) {
357             caught = true;
358         }
359         assertTrue(caught);
360     }
361 
362     @Test
canSpecifyResponseCharset()363     public void canSpecifyResponseCharset() throws IOException {
364         // both these docs have <80> in there as euro/control char depending on charset
365         String noCharsetUrl = "http://direct.infohound.net/tools/Windows-1252-nocharset.html";
366         String charsetUrl = "http://direct.infohound.net/tools/Windows-1252-charset.html";
367 
368         // included in meta
369         Connection.Response res1 = Jsoup.connect(charsetUrl).execute();
370         assertNull(res1.charset()); // not set in headers
371         final Document doc1 = res1.parse();
372         assertEquals("windows-1252", doc1.charset().displayName()); // but determined at parse time
373         assertEquals("Cost is €100", doc1.select("p").text());
374         assertTrue(doc1.text().contains("€"));
375 
376         // no meta, no override
377         Connection.Response res2 = Jsoup.connect(noCharsetUrl).execute();
378         assertNull(res2.charset()); // not set in headers
379         final Document doc2 = res2.parse();
380         assertEquals("UTF-8", doc2.charset().displayName()); // so defaults to utf-8
381         assertEquals("Cost is �100", doc2.select("p").text());
382         assertTrue(doc2.text().contains("�"));
383 
384         // no meta, let's override
385         Connection.Response res3 = Jsoup.connect(noCharsetUrl).execute();
386         assertNull(res3.charset()); // not set in headers
387         res3.charset("windows-1252");
388         assertEquals("windows-1252", res3.charset()); // read back
389         final Document doc3 = res3.parse();
390         assertEquals("windows-1252", doc3.charset().displayName()); // from override
391         assertEquals("Cost is €100", doc3.select("p").text());
392         assertTrue(doc3.text().contains("€"));
393     }
394 
395     @Test
handlesUnescapedRedirects()396     public void handlesUnescapedRedirects() throws IOException {
397         // URL locations should be url safe (ascii) but are often not, so we should try to guess
398         // in this case the location header is utf-8, but defined in spec as iso8859, so detect, convert, encode
399         String url = "https://direct.infohound.net/tools/302-utf.pl";
400         String urlEscaped = "https://direct.infohound.net/tools/test%F0%9F%92%A9.html";
401 
402         Connection.Response res = Jsoup.connect(url).execute();
403         Document doc = res.parse();
404         assertEquals(doc.body().text(), "\uD83D\uDCA9!");
405         assertEquals(doc.location(), urlEscaped);
406 
407         Connection.Response res2 = Jsoup.connect(url).followRedirects(false).execute();
408         assertEquals("/tools/test\uD83D\uDCA9.html", res2.header("Location"));
409         // if we didn't notice it was utf8, would look like: Location: /tools/test💩.html
410     }
411 
handlesEscapesInRedirecct()412     @Test public void handlesEscapesInRedirecct() throws IOException {
413         Document doc = Jsoup.connect("https://direct.infohound.net/tools/302-escaped.pl").get();
414         assertEquals("https://direct.infohound.net/tools/q.pl?q=one%20two", doc.location());
415 
416         doc = Jsoup.connect("https://direct.infohound.net/tools/302-white.pl").get();
417         assertEquals("https://direct.infohound.net/tools/q.pl?q=one+two", doc.location());
418     }
419 
420     @Test
handlesUt8fInUrl()421     public void handlesUt8fInUrl() throws IOException {
422         String url = "https://direct.infohound.net/tools/test\uD83D\uDCA9.html";
423         String urlEscaped = "https://direct.infohound.net/tools/test%F0%9F%92%A9.html";
424 
425         Connection.Response res = Jsoup.connect(url).execute();
426         Document doc = res.parse();
427         assertEquals("\uD83D\uDCA9!", doc.body().text());
428         assertEquals(urlEscaped, doc.location());
429     }
430 
431     @Test
inWildUtfRedirect()432     public void inWildUtfRedirect() throws IOException {
433         Connection.Response res = Jsoup.connect("http://brabantn.ws/Q4F").execute();
434         Document doc = res.parse();
435         assertEquals(
436             "http://www.omroepbrabant.nl/?news/2474781303/Gestrande+ree+in+Oss+niet+verdoofd,+maar+doodgeschoten+%E2%80%98Dit+kan+gewoon+niet,+bizar%E2%80%99+[VIDEO].aspx",
437             doc.location()
438             );
439     }
440 
441     @Test
inWildUtfRedirect2()442     public void inWildUtfRedirect2() throws IOException {
443         Connection.Response res = Jsoup.connect("https://ssl.souq.com/sa-en/2724288604627/s").execute();
444         Document doc = res.parse();
445         assertEquals(
446             "https://saudi.souq.com/sa-en/%D8%AE%D8%B2%D9%86%D8%A9-%D8%A2%D9%85%D9%86%D8%A9-3-%D8%B7%D8%A8%D9%82%D8%A7%D8%AA-%D8%A8%D9%86%D8%B8%D8%A7%D9%85-%D9%82%D9%81%D9%84-%D8%A5%D9%84%D9%83%D8%AA%D8%B1%D9%88%D9%86%D9%8A-bsd11523-6831477/i/?ctype=dsrch",
447             doc.location()
448         );
449     }
450 
handlesEscapedRedirectUrls()451    @Test public void handlesEscapedRedirectUrls() throws IOException {
452         String url = "http://www.altalex.com/documents/news/2016/12/06/questioni-civilistiche-conseguenti-alla-depenalizzazione";
453         // sends: Location:http://shop.wki.it/shared/sso/sso.aspx?sso=&url=http%3a%2f%2fwww.altalex.com%2fsession%2fset%2f%3freturnurl%3dhttp%253a%252f%252fwww.altalex.com%253a80%252fdocuments%252fnews%252f2016%252f12%252f06%252fquestioni-civilistiche-conseguenti-alla-depenalizzazione
454         // then to: http://www.altalex.com/session/set/?returnurl=http%3a%2f%2fwww.altalex.com%3a80%2fdocuments%2fnews%2f2016%2f12%2f06%2fquestioni-civilistiche-conseguenti-alla-depenalizzazione&sso=RDRG6T684G4AK2E7U591UGR923
455         // then : http://www.altalex.com:80/documents/news/2016/12/06/questioni-civilistiche-conseguenti-alla-depenalizzazione
456 
457         // bug is that jsoup goes to
458         // 	GET /shared/sso/sso.aspx?sso=&url=http%253a%252f%252fwww.altalex.com%252fsession%252fset%252f%253freturnurl%253dhttp%25253a%25252f%25252fwww.altalex.com%25253a80%25252fdocuments%25252fnews%25252f2016%25252f12%25252f06%25252fquestioni-civilistiche-conseguenti-alla-depenalizzazione HTTP/1.1
459         // i.e. double escaped
460 
461         Connection.Response res = Jsoup.connect(url)
462                 .proxy("localhost", 8888)
463                 .execute();
464         Document doc = res.parse();
465         assertEquals(200, res.statusCode());
466     }
467 
handlesUnicodeInQuery()468     @Test public void handlesUnicodeInQuery() throws IOException {
469         Document doc = Jsoup.connect("https://www.google.pl/search?q=gąska").get();
470         assertEquals("gąska - Szukaj w Google", doc.title());
471 
472         doc = Jsoup.connect("http://mov-world.net/archiv/TV/A/%23No.Title/").get();
473         assertEquals("Index of /archiv/TV/A/%23No.Title", doc.title());
474     }
475 
handlesSuperDeepPage()476     @Test public void handlesSuperDeepPage() throws IOException {
477         // https://github.com/jhy/jsoup/issues/955
478 
479         long start = System.currentTimeMillis();
480         String url = "http://sv.stargate.wikia.com/wiki/M2J";
481         Document doc = Jsoup.connect(url).get();
482         assertEquals("M2J | Sv.stargate Wiki | FANDOM powered by Wikia", doc.title());
483         assertEquals(110160, doc.select("dd").size());
484         // those are all <dl><dd> stacked in each other. wonder how that got generated?
485         assertTrue(System.currentTimeMillis() - start < 1000);
486     }
487 
488     @Test public void handles966() throws IOException {
489         // http://szshb.nxszs.gov.cn/
490         // https://github.com/jhy/jsoup/issues/966
491 
492         Document doc = Jsoup.connect("http://szshb.nxszs.gov.cn/").get();
493 
494         assertEquals("石嘴山市环境保护局", doc.title());
495     }
496 
497     @Test public void canRequestIdn() throws IOException {
498         String url = "https://räksmörgås.josefsson.org/";
499         Document doc = Jsoup.connect(url).get();
500 
501         assertEquals("https://xn--rksmrgs-5wao1o.josefsson.org/", doc.location());
502         assertTrue(doc.title().contains("Räksmörgås.josefßon.org"));
503     }
504 
505 }
506