Skip to content

fix: per-segment article path encoding + /articles/search - #12

Open
niclydon wants to merge 1 commit into
nickytonline:mainfrom
niclydon:fix/article-path-and-search-urls
Open

niclydon wants to merge 1 commit into
nickytonline:mainfrom
niclydon:fix/article-path-and-search-urls

Conversation

@niclydon

@niclydon niclydon commented Jul 18, 2026

Copy link
Copy Markdown

Summary

  • Article by path: encode username and slug as separate path segments. Encoding the whole user/slug with encodeURIComponent produces a single segment with %2F, which Forem 404s.
  • Search: call GET /articles/search instead of dead search/feed_content (live 404).
  • Unit tests for both rules without network.

Context

Hit these in production against live Forem while operating a forked MCP. Happy to contribute the fix upstream so other agents don't get silent 404s.

Test plan

  • npm run test:ci (vitest) green
  • Live: getArticle({ path: "user/slug" }) → 200
  • Live: searchArticles({ q: "python" }) → 200

Summary by CodeRabbit

  • Bug Fixes
    • Corrected article URL generation for numeric IDs and username/slug paths.
    • Improved handling of leading slashes and blocked unsafe path traversal.
    • Updated article search requests to use the supported search endpoint and parameters.
    • Prevented unsupported search options from being sent.
  • New Features
    • Added reliable URL helpers for article and search links.
    • Exposed the resolved API base URL for scripting and integrations.
  • Tests
    • Added coverage for URL construction, validation, search parameters, and path safety.

Forem returns 404 when username/slug is encodeURIComponent'd as one
segment (%2F). Encode each segment separately.

search/feed_content is dead on live Forem; use official GET
/articles/search instead. Unit tests cover both rules without network.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds validated article and search URL helpers, exposes DevToAPI’s base URL, routes article and search requests through the helpers, and adds tests for paths, query parameters, encoding, and validation.

Changes

DEV.to URL construction and API integration

Layer / File(s) Summary
URL builders and validation
src/devto-api.ts, src/devto-api.test.ts
Adds validated, segment-safe article URL construction and official search URL construction with supported query parameters. Tests cover paths, encoding, traversal prevention, and query output.
DevToAPI request integration
src/devto-api.ts, src/devto-api.test.ts
Exposes baseUrl and updates getArticle and searchArticles to use the new helpers. Tests compare public article URL behavior with the helper.گ

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant DevToAPI
  participant URLHelpers
  participant DEVTOAPI
  Caller->>DevToAPI: call getArticle or searchArticles
  DevToAPI->>URLHelpers: build request URL
  URLHelpers-->>DevToAPI: return endpoint URL
  DevToAPI->>DEVTOAPI: send API request
  DEVTOAPI-->>DevToAPI: return response
  DevToAPI-->>Caller: return API result
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: per-segment article path encoding and switching search to /articles/search.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/devto-api.ts (1)

178-184: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Expose the helper’s top parameter through searchArticles.

buildSearchArticlesUrl supports top, but this public method’s argument type omits it, preventing typed callers from using that supported search parameter.

   async searchArticles(args: {
     q: string;
     page?: number;
     per_page?: number;
+    top?: number;
     search_fields?: string;
   }): Promise<unknown> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devto-api.ts` around lines 178 - 184, Update the searchArticles argument
type to include the optional top parameter supported by buildSearchArticlesUrl,
preserving its existing type and forwarding behavior through args so typed
callers can use it.
🧹 Nitpick comments (1)
src/devto-api.test.ts (1)

67-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise getArticle rather than only rebuilding its expected URL.

This test never calls the public method, so it cannot detect a regression in the helper wiring. Mock globalThis.fetch, call await api.getArticle(...), and assert the URL passed to the mock.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devto-api.test.ts` around lines 67 - 75, Update the test in “DevToAPI URL
helpers via public methods” to mock globalThis.fetch, invoke api.getArticle with
the article path, and assert the URL passed to the fetch mock. Keep the existing
URL expectations while ensuring the test exercises the public getArticle method
and its helper wiring.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/devto-api.ts`:
- Around line 90-93: Update the DevTo API class’s baseUrl getter to return a new
URL copy derived from `#baseUrl` rather than the private URL instance, while
preserving the existing URL value and getter interface.
- Around line 30-46: Update the article path validation around the cleaned path
construction to split on "/" and require exactly two non-empty segments,
rejecting values such as "user/slug/extra" before URL encoding. Preserve
independent encoding of the username and slug, and add a test covering the
rejected three-segment path.

---

Outside diff comments:
In `@src/devto-api.ts`:
- Around line 178-184: Update the searchArticles argument type to include the
optional top parameter supported by buildSearchArticlesUrl, preserving its
existing type and forwarding behavior through args so typed callers can use it.

---

Nitpick comments:
In `@src/devto-api.test.ts`:
- Around line 67-75: Update the test in “DevToAPI URL helpers via public
methods” to mock globalThis.fetch, invoke api.getArticle with the article path,
and assert the URL passed to the fetch mock. Keep the existing URL expectations
while ensuring the test exercises the public getArticle method and its helper
wiring.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54e4b15a-e282-45d5-bd7a-761d09f7f9ae

📥 Commits

Reviewing files that changed from the base of the PR and between db4f9bd and 159bdb2.

📒 Files selected for processing (2)
  • src/devto-api.test.ts
  • src/devto-api.ts

Comment thread src/devto-api.ts
Comment on lines +30 to +46
if (args.path) {
const cleaned = args.path.replace(/^\/+/, "").replace(/\/+$/, "");
if (!cleaned || cleaned.includes("..") || cleaned.includes("://")) {
throw new Error("Invalid article path");
}
const slash = cleaned.indexOf("/");
if (slash <= 0 || slash === cleaned.length - 1) {
throw new Error(
'Article path must be "username/slug" (two path segments)',
);
}
const username = cleaned.slice(0, slash);
const slug = cleaned.slice(slash + 1);
// Encode each segment independently so "/" remains a path separator.
return new URL(
`articles/${encodeURIComponent(username)}/${encodeURIComponent(slug)}`,
baseUrl,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject paths containing more than two segments.

user/slug/extra passes validation and becomes user/slug%2Fextra, despite the documented two-segment contract. Split first and require exactly two non-empty segments.

Proposed fix
-    const slash = cleaned.indexOf("/");
-    if (slash <= 0 || slash === cleaned.length - 1) {
+    const segments = cleaned.split("/");
+    if (segments.length !== 2 || segments.some((segment) => !segment)) {
       throw new Error(
         'Article path must be "username/slug" (two path segments)',
       );
     }
-    const username = cleaned.slice(0, slash);
-    const slug = cleaned.slice(slash + 1);
+    const [username, slug] = segments;

Also add a rejection test for "user/slug/extra".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (args.path) {
const cleaned = args.path.replace(/^\/+/, "").replace(/\/+$/, "");
if (!cleaned || cleaned.includes("..") || cleaned.includes("://")) {
throw new Error("Invalid article path");
}
const slash = cleaned.indexOf("/");
if (slash <= 0 || slash === cleaned.length - 1) {
throw new Error(
'Article path must be "username/slug" (two path segments)',
);
}
const username = cleaned.slice(0, slash);
const slug = cleaned.slice(slash + 1);
// Encode each segment independently so "/" remains a path separator.
return new URL(
`articles/${encodeURIComponent(username)}/${encodeURIComponent(slug)}`,
baseUrl,
if (args.path) {
const cleaned = args.path.replace(/^\/+/, "").replace(/\/+$/, "");
if (!cleaned || cleaned.includes("..") || cleaned.includes("://")) {
throw new Error("Invalid article path");
}
const segments = cleaned.split("/");
if (segments.length !== 2 || segments.some((segment) => !segment)) {
throw new Error(
'Article path must be "username/slug" (two path segments)',
);
}
const [username, slug] = segments;
// Encode each segment independently so "/" remains a path separator.
return new URL(
`articles/${encodeURIComponent(username)}/${encodeURIComponent(slug)}`,
baseUrl,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devto-api.ts` around lines 30 - 46, Update the article path validation
around the cleaned path construction to split on "/" and require exactly two
non-empty segments, rejecting values such as "user/slug/extra" before URL
encoding. Preserve independent encoding of the username and slug, and add a test
covering the rejected three-segment path.

Comment thread src/devto-api.ts
Comment on lines +90 to +93
/** Exposed for unit tests and smoke scripts. */
get baseUrl(): URL {
return this.#baseUrl;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Return a copy instead of exposing mutable request state.

URL is mutable, so api.baseUrl.hostname = ... silently redirects all subsequent API requests despite #baseUrl being private.

Proposed fix
   get baseUrl(): URL {
-    return this.#baseUrl;
+    return new URL(this.#baseUrl);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Exposed for unit tests and smoke scripts. */
get baseUrl(): URL {
return this.#baseUrl;
}
/** Exposed for unit tests and smoke scripts. */
get baseUrl(): URL {
return new URL(this.#baseUrl);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devto-api.ts` around lines 90 - 93, Update the DevTo API class’s baseUrl
getter to return a new URL copy derived from `#baseUrl` rather than the private
URL instance, while preserving the existing URL value and getter interface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant