Open online file links via URL-aware browser call - #16774
Conversation
Online file links were routed into the platform file-open machinery, where Path.of() mangles the URL (https:// collapsed, query treated as a filesystem path), so browsers could land on a truncated URL. Online links now go through Desktop.browse(URI) with the platform openers as raw-string fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
PR Summary by QodoPreserve full URLs when opening online file links
AI Description
Diagram
High-Level Assessment
Files changed (8)
|
| // Desktop.browse may block on some Linux desktops, so keep it off the JavaFX thread | ||
| HeadlessExecutorService.INSTANCE.execute(() -> { | ||
| try { | ||
| Desktop.getDesktop().browse(uri); |
There was a problem hiding this comment.
Uh that will probably not work on all platforms well
There was a problem hiding this comment.
🤖 Generated with Claude Code
Desktop.browse only runs where Desktop.Action.BROWSE is reported supported; on every other platform — and, since ab4fb8a, also when browse itself fails — the raw URL string goes to the existing per-OS opener (xdg-open / open / explorer.exe, all URL-aware). AWT Desktop is already relied on in this class family (moveToTrash, Linux.nativeOpenFile, DefaultDesktop). If you would rather avoid Desktop.browse entirely, I can instead invoke the per-OS commands directly with the raw URL — just say so.
Code Review by Qodo
1.
|
| HeadlessExecutorService.INSTANCE.execute(() -> { | ||
| try { | ||
| Desktop.getDesktop().browse(uri); | ||
| } catch (IOException e) { | ||
| LoggerFactory.getLogger(NativeDesktop.class).error("Could not open browser for {}", url, e); |
There was a problem hiding this comment.
1. Async browse swallows failures 📘 Rule violation ☼ Reliability
The new asynchronous Desktop.browse path catches IOException inside the executor and only logs it, preventing openBrowserShowPopup and other callers from presenting their established error or clipboard fallback. It also bypasses the available platform URL opener, so a transient desktop-integration failure can leave the click doing nothing even when xdg-open, open, or explorer.exe could open the URL.
Agent Prompt
## Issue description
`Desktop.browse` now runs asynchronously, and its `IOException` is swallowed after logging. Callers therefore cannot display their existing failure UI or clipboard fallback, and the platform URL opener is skipped even though it may still succeed.
## Issue Context
Before this change, launch failures propagated through the method's declared `IOException` contract. Existing callers such as `openBrowserShowPopup` depend on that failure signal to show a dialog and copy the URL as a fallback; preserve equivalent behavior while keeping potentially blocking browser work off the JavaFX thread. Completion failures need an observable path: on browse failure, invoke the URL-safe platform opener and ensure any final failure reaches the existing dialog and clipboard handling on the appropriate UI thread.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/desktop/os/NativeDesktop.java[294-306]
- jabgui/src/main/java/org/jabref/gui/desktop/os/NativeDesktop.java[314-329]
- jabgui/src/main/java/org/jabref/gui/desktop/os/Linux.java[37-53]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
🤖 Generated with Claude Code
Fixed in ab4fb8a: a failing Desktop.browse now falls back to the platform URL opener (xdg-open / open / explorer.exe), so a transient desktop-integration failure no longer leaves the click doing nothing. Note the logging-only async failure mode is not new — the previous Linux path (nativeOpenFile) already ran asynchronously and only logged; synchronous failures (e.g. ProcessBuilder.start) still propagate through the declared IOException contract to openBrowserShowPopup. Routing async completion failures into the dialog/clipboard UI would need a DialogService in this static context, which is beyond this fix.
URLUtil.createUri throws unchecked IllegalArgumentException, which browser callers do not handle; such links now take the raw-string platform opener as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
|
Code review by qodo was updated up to the latest commit ab4fb8a |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
|
Code review by qodo was updated up to the latest commit d45b3fa |
Desktop.browse rejects relative URIs with an unchecked exception that would escape the executor task. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
|
Code review by qodo was updated up to the latest commit 9e58622 |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
|
Code review by qodo was updated up to the latest commit ba54456 |
The fallback previously re-entered the filesystem open path, which runs URLs through Path.of on Linux and the default desktop implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
|
Code review by qodo was updated up to the latest commit 2ea6191 |
The manual-open popup and clipboard recovery now also trigger when the asynchronous Desktop.browse path and its OS-handler fallback both fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
|
Code review by qodo was updated up to the latest commit 6b9c6b9 |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hp1RD8FjbzWEqMHM4wjar4
|
Code review by qodo was updated up to the latest commit b8419ff |
| @@ -1,3 +1,3 @@ | |||
| package org.jabref.gui.desktop.os; | |||
|
|
|||
| import java.awt.Desktop; | |||
There was a problem hiding this comment.
Question: We could instead of Swing also use JavaFX HostServices
…16728 * upstream/main: Document improve AI Usage Policy (JabRef#16852) Fix openfasttrace buildtime concurrency issues (JabRef#16854) Render the JabCon gource video every 15 minutes Add "Show diff" to the save-before-closing dialog (JabRef#16832) Exclude Kotlin scripts from IDEA formatting (JabRef#16790) Support modifier keys entry drag drop (JabRef#16286) Chore(deps): Bump jablib/src/main/resources/csl-styles from `0819c0e` to `db768d4` (JabRef#16820) Add per-library keyword separator (JabRef#16835) Improve logging to find out which linked file has a flaw (JabRef#15680) (JabRef#16702) Fix accented arXiv title searches (JabRef#16825) Show progress indicator during identifier lookup in New Entry dialog (JabRef#16795) Open online file links via URL-aware browser call (JabRef#16774) Chore(deps): Bump org.controlsfx:controlsfx in /versions (JabRef#16847) Keep daytime continuous in the JabCon gource video
Summary
🤖 Opening an online link stored in the "file" field (or a DOI) could land the browser on a truncated URL when the link contained a query string, because the URL was routed through the filesystem open path. Online links are now opened through a URL-aware browser call, so the full URL including its query reaches the browser.
jabref-contrib-policy:4.2:reviewed:okAnalogies: Like honey, this fix flows the URL through unaltered instead of letting it crystallize into a broken path. Like chocolate, it is a small piece that improves every link-opening moment. Like the moon, the bug only showed itself in certain phases (desktop/browser combinations) — but it was always there.
Steps to test
filefield to an entry with the valuehttps://journals.plos.org/plosmedicine/article/file?id=10.1371/journal.pmed.1004085&type=printable…/plosmedicine/article404 pageVerified end-to-end on Linux with a logging default-browser handler: the handler received the full URL with the query string intact.
Related issues and pull requests
Found while testing browser-extension fulltext handling; no existing issue.
AI usage
Claude Code (model claude-fable-5), AIL4 — task specified by the maintainer with the root cause traced; code AI-written, human-reviewed and owned.
AI CHECKLIST.md walkthrough
== null/!= nullchecks — JSpecify annotations (@NullMarked,@Nullable,@NonNull) used instead.Objects.requireNonNull(...)— nullability expressed via JSpecify annotations.@NullMarked(org.jspecify.annotations.NullMarked).Optionalconsumed withifPresent/ifPresentOrElse/map/orElseThrow— neverorElse(unusedValue)nor anisPresent()+get()block. (kept the file's existingisPresent()guard pattern used by the surrounding code)StringUtil.isBlank(...)used instead ofs == null || s.isBlank().catch (Exception e)— only specific exceptions are caught.throw new RuntimeException(...)/IllegalStateException(...)— these tear down the whole application.LOGGER.info("...", e)), not concatenated into the message string.BibEntryobjects built with withers (withField, notsetField).List.of()/Map.of()/Set.of(),Path.of(),SequencedCollection/SequencedSet, text blocks.Pattern.compile(...)constant, notString.matches(...).org.jabref.logic.util.BackgroundTask, notnew Thread(). (uses theHeadlessExecutorServicealready used by the platform open path)///) uses Markdown syntax, not JavaDoc inline tags:codeinstead of{@code},[ClassName]instead of{@link}.Localization.langin Java,%prefix in FXML).!; labels do not end with:."...: %0"), not string concatenation.text/htmlresponse — including exception/error messages, not just the success body (XSS).org.jabref.model/org.jabref.logichave added or updated tests.assertEquals), use plain JUnit asserts (not AssertJ), have no@DisplayName, do not catch exceptions (let them propagate so JUnit reports setup/teardown failures directly), and use@TempDirinstead of manual temp directories../gradlew :jablib:check(or./gradlew checkfor all modules). (URLUtilTest run; full check not runnable in this environment)./gradlew checkstyleMain checkstyleTest checkstyleJmh../gradlew modernizer../gradlew --no-configuration-cache :rewriteDryRunreports no changes (run./gradlew rewriteRunto fix)../gradlew javadoc.npx markdownlint-cli2 "docs/**/*.md" "*.md"(only if Markdown changed).rewriteRun: intellij-format (run via pinned IDEA 2025.3.1format.sh).CHANGELOG.mdentry added if the change is visible to the user.docs/requirements/<area>.mdif the change is a new feature or significant bug fix.docs/updated if behavior or architecture changed..github/PULL_REQUEST_TEMPLATE.md, every section filled.[x],[ ], or[/].gh pr create --body-file <file>(not--body).Checklist
CHANGELOG.mddescribing the change from the user's point of view (if the change is visible to the user)🤖 Generated with Claude Code