Skip to content

Conversation

@joshfester
Copy link

This allows usage in environments where you only have the HTML and none of the assets

@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

Walkthrough

This PR introduces a new optional remote configuration option to Beasties that enables downloading and inlining remote stylesheets (HTTP, HTTPS, or protocol-relative URLs) when explicitly enabled. The change includes type definitions, documentation updates, implementation logic with error handling, and test coverage for both enabled and disabled states.

Changes

Cohort / File(s) Summary
Documentation
packages/beasties/README.md
Added remote option documentation describing it as a boolean flag to download and inline remote stylesheets with default value false.
Type Definitions
packages/beasties/src/index.d.ts, packages/beasties/src/types.ts
Added remote?: boolean property to the Options interface to expose the new configuration option.
Implementation
packages/beasties/src/index.ts
Added remote stylesheet fetching logic in getCssAsset() that detects remote hrefs and conditionally fetches them when enabled; refined embedLinkedStylesheet() to strip query parameters and fragments before CSS extension validation.
Tests
packages/beasties/test/beasties.test.ts
Added two test cases: one verifying remote stylesheets are ignored by default, another verifying remote CSS is fetched and inlined when the option is enabled.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Beasties
    participant getCssAsset
    participant Remote Server

    Client->>Beasties: Process stylesheet with remote option
    Beasties->>getCssAsset: getCssAsset(href)
    
    alt Remote URL detected
        alt remote option enabled
            getCssAsset->>Remote Server: fetch(remote URL)
            alt fetch successful & 2xx response
                Remote Server-->>getCssAsset: CSS text
                getCssAsset-->>Beasties: return CSS
            else fetch error or non-2xx
                getCssAsset->>getCssAsset: log warning
                getCssAsset-->>Beasties: return undefined
            end
        else remote option disabled
            getCssAsset-->>Beasties: return undefined
        end
    else Local URL
        getCssAsset-->>Beasties: process normally
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Focus areas requiring attention:
    • HTTP response status validation and error handling in getCssAsset()
    • Protocol-relative URL normalisation logic
    • Query parameter and fragment stripping in embedLinkedStylesheet()
    • Fetch error scenarios and logging behaviour
    • Test coverage completeness for edge cases (timeouts, malformed URLs, non-CSS responses)

Possibly related PRs

  • Fix filesystem path loading #30: Modifies getCssAsset() logic for stylesheet loading, adding remote-fetch handling that complements existing filesystem-based readFile changes.

Suggested reviewers

  • danielroe
  • alan-agius4

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description relates to the changeset by explaining the use case for the new remote option: enabling usage in environments with only HTML and no assets.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately summarises the main change: adding a 'remote' option to enable downloading remote stylesheets, which aligns with the core functionality introduced across all modified files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/beasties/test/beasties.test.ts (1)

408-435: Consider restoring the mocked fetch.

The test correctly validates remote stylesheet fetching. However, the globalThis.fetch mock could potentially affect other tests if not properly isolated.

Consider using vitest's cleanup utilities:

  it('fetches remote stylesheets when remote: true', async () => {
    const beasties = new Beasties({
      reduceInlineStyles: false,
      remote: true,
    })

    // Mock fetch
+   const originalFetch = globalThis.fetch
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      text: async () => 'h1 { color: blue; } h2.unused { color: red; }',
    })
    globalThis.fetch = mockFetch as any

    const result = await beasties.process(trim`
      <html>
        <head>
          <link rel="stylesheet" href="https://example.com/style.css">
        </head>
        <body>
          <h1>Hello World!</h1>
        </body>
      </html>
    `)

    expect(mockFetch).toHaveBeenCalledWith('https://example.com/style.css')
    expect(result).toContain('<style>h1{color:blue}</style>')
    expect(result).toContain('https://example.com/style.css')
+
+   // Restore original fetch
+   globalThis.fetch = originalFetch
  })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bb8e58 and ae62a44.

📒 Files selected for processing (5)
  • packages/beasties/README.md (1 hunks)
  • packages/beasties/src/index.d.ts (1 hunks)
  • packages/beasties/src/index.ts (2 hunks)
  • packages/beasties/src/types.ts (1 hunks)
  • packages/beasties/test/beasties.test.ts (1 hunks)
🔇 Additional comments (5)
packages/beasties/src/index.d.ts (1)

53-53: LGTM!

The type definition for the remote option is correctly declared and follows the existing pattern for optional boolean properties.

packages/beasties/src/types.ts (1)

43-46: LGTM!

The documentation is clear and accurately describes the feature, including all supported URL formats and the secure default value.

packages/beasties/test/beasties.test.ts (1)

387-406: LGTM!

The test correctly validates that remote stylesheets are ignored by default, ensuring the secure-by-default behavior.

packages/beasties/README.md (1)

123-123: LGTM!

The documentation clearly describes the remote option and is consistent with the type definitions.

packages/beasties/src/index.ts (1)

274-278: LGTM!

The addition of query parameter and fragment stripping before checking the file extension is a solid improvement that allows CSS files with query strings or hashes to be properly processed.

Comment on lines +184 to 207
// Handle remote stylesheets
const isRemote = /^https?:\/\//.test(href) || href.startsWith('//')
if (isRemote) {
if (this.options.remote === true) {
try {
// Normalize protocol-relative URLs
const absoluteUrl = href.startsWith('//') ? `https:${href}` : href

const response = await fetch(absoluteUrl)

if (!response.ok) {
this.logger.warn?.(`Failed to fetch ${absoluteUrl} (${response.status})`)
return undefined
}

return await response.text()
}
catch (error) {
this.logger.warn?.(`Error fetching ${href}: ${(error as Error).message}`)
return undefined
}
}
return undefined
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Document the security implications of enabling remote stylesheet fetching.

The implementation correctly handles remote stylesheets with proper error handling. However, when remote: true is enabled with untrusted HTML input, this could allow Server-Side Request Forgery (SSRF) attacks by making requests to internal network resources.

Consider adding a security note to the documentation:

In packages/beasties/README.md, add a security section:

### Security Considerations

**Remote Stylesheet Fetching**

The `remote` option enables fetching stylesheets from external URLs. Only enable this option if:
- The HTML input is from a trusted source
- You have validated the URLs in the HTML
- Your application is not exposed to SSRF attacks via internal network access

For untrusted HTML input, consider implementing URL validation or an allowlist of permitted domains.

Alternatively, consider implementing URL validation directly in the code:

// Add validation before fetching
const url = new URL(absoluteUrl)
if (url.hostname === 'localhost' || url.hostname.startsWith('127.') || url.hostname.startsWith('192.168.')) {
  this.logger.warn?.(`Blocked internal URL: ${absoluteUrl}`)
  return undefined
}
🤖 Prompt for AI Agents
In packages/beasties/src/index.ts around lines 184 to 207, the remote stylesheet
fetch path allows SSRF when remote: true; update the code to validate
absoluteUrl before fetching (parse with URL and block or warn+return for
internal hosts/IPs such as localhost, 127.*, 10.*, 172.16-31.*, 192.168.* and
optionally support a configurable allowlist of safe hostnames), and log blocked
URLs; additionally, add a Security Considerations section to
packages/beasties/README.md documenting the risks of enabling remote:true,
recommending only using it for trusted HTML, validating URLs or using an
allowlist, and describing the behavior when a URL is blocked.

@danielroe danielroe changed the title Add 'remote' option to download remote stylesheets feat: add remote option to download remote stylesheets Nov 18, 2025
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