Skip to content

Cache the api docs#434

Merged
w3nl merged 4 commits intomainfrom
feature/cache-api-docs
Jul 30, 2025
Merged

Cache the api docs#434
w3nl merged 4 commits intomainfrom
feature/cache-api-docs

Conversation

@w3nl
Copy link
Copy Markdown
Contributor

@w3nl w3nl commented Jul 30, 2025

Summary by CodeRabbit

  • New Features

    • Improved the API documentation endpoint with HTTP caching and ETag support for more efficient responses and reduced bandwidth usage.
  • Tests

    • Enhanced tests to verify correct caching behavior and conditional GET support on the API documentation endpoint.

@w3nl w3nl requested a review from Copilot July 30, 2025 08:39
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jul 30, 2025

Walkthrough

The /api-docs endpoint handler was updated to implement HTTP caching using ETag headers and cache control, enabling conditional GET support. Corresponding tests were enhanced to verify the correct handling of ETag and Cache-Control headers, as well as the server's response to conditional requests.

Changes

Cohort / File(s) Change Summary
API Docs Caching Implementation
src/api.js
Enhanced /api-docs endpoint to generate ETag from the API spec, check If-None-Match, and set appropriate caching headers, supporting conditional GET and cache control.
API Docs Caching Tests
src/server.test.js
Updated tests to verify presence of Cache-Control and ETag headers, and to assert correct 304 response when If-None-Match matches.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐇
Hopping along the docs' bright lane,
ETags shimmer, caching’s gain.
If-None-Match, the server replies,
"No changes here!"—a clever surprise.
Tests confirm with gentle cheer,
That docs are swift and crystal clear!

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2f61be and c796904.

📒 Files selected for processing (1)
  • src/api.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/api.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/cache-api-docs

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

This comment was marked as outdated.

Copy link
Copy Markdown

@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: 2

🧹 Nitpick comments (2)
src/api.js (1)

85-98: Consider ETag generation performance implications.

Generating the ETag on every API instance creation by serializing the entire specification to JSON and base64 encoding it could be expensive for large specifications. Consider generating it once during initialization or using a more efficient hashing approach.

Consider this alternative approach for better performance:

+  constructor ({
+    // ... existing parameters
+  }) {
+    // ... existing assignments
+    
+    // Generate ETag once during initialization
+    if (this.apiDocs) {
+      const apiDocsString = JSON.stringify(this.specification)
+      this.etag = `"${Buffer.from(apiDocsString).toString('base64')}"`
+    }
+  }

   setup () {
     // ... existing code

     if (this.apiDocs) {
-      // Generate an ETag for the specification (simple hash or JSON string)
-      const apiDocsString = JSON.stringify(this.specification)
-      const etag = `"${Buffer.from(apiDocsString).toString('base64')}"`
-
       router.get('/api-docs', (request, response) => {
         // Check for If-None-Match header
-        if (request.headers['if-none-match'] === etag) {
+        if (request.headers['if-none-match'] === this.etag) {
           response.status(304).end()
           return
         }
         response.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate')
-        response.setHeader('ETag', etag)
+        response.setHeader('ETag', this.etag)
         response.json(this.specification)
       })
     }
src/server.test.js (1)

95-109: Consider adding edge case tests for caching.

While the current test covers the happy path, consider adding tests for edge cases such as:

  • Invalid ETag format in If-None-Match header
  • Multiple ETag values in If-None-Match header
  • Weak vs strong ETag comparison

Here's an example of additional test cases:

await t.test(
  'It should handle invalid ETag in If-None-Match header',
  async () => {
    const response = await request
      .get('/v1/api-docs')
      .set('If-None-Match', 'invalid-etag')
    
    assert.strictEqual(response.status, 200)
    assert.ok(response.headers['etag'])
  }
)

await t.test(
  'It should handle multiple ETags in If-None-Match header',
  async () => {
    const firstResponse = await request.get('/v1/api-docs')
    const etag = firstResponse.headers['etag']
    
    const response = await request
      .get('/v1/api-docs')
      .set('If-None-Match', `${etag}, "other-etag"`)
    
    assert.strictEqual(response.status, 304)
  }
)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7518c84 and 4953f0e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • src/api.js (1 hunks)
  • src/server.test.js (1 hunks)
🧰 Additional context used
🪛 GitHub Check: build
src/api.js

[failure] 93-93:
Expected indentation of 10 spaces but found 8


[failure] 93-93:
Expected indentation of 10 spaces but found 8


[failure] 92-92:
Expected indentation of 10 spaces but found 8


[failure] 92-92:
Expected indentation of 10 spaces but found 8


[failure] 89-89:
Expected indentation of 6 spaces but found 4


[failure] 89-89:
Expected indentation of 6 spaces but found 4


[failure] 87-87:
Expected indentation of 6 spaces but found 4


[failure] 87-87:
Expected indentation of 6 spaces but found 4


[failure] 86-86:
Expected indentation of 6 spaces but found 4


[failure] 86-86:
Expected indentation of 6 spaces but found 4

🪛 GitHub Actions: Node Lint
src/api.js

[error] 86-86: ESLint: Expected indentation of 6 spaces but found 4. (indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (2)
src/api.js (1)

95-95: Validate cache control settings for API documentation.

The cache settings (public, max-age=3600, must-revalidate) are appropriate for API documentation. The 1-hour cache duration with revalidation strikes a good balance between performance and freshness for API specs.

src/server.test.js (1)

92-110: Excellent test coverage for caching functionality.

The test comprehensively validates the caching implementation by:

  • Verifying initial response includes Cache-Control and ETag headers
  • Testing conditional GET request with If-None-Match header
  • Confirming 304 Not Modified response with empty body

The test structure is clear and covers the essential caching scenarios.

Comment thread src/api.js Outdated
Comment on lines +85 to +87
// Generate an ETag for the specification (simple hash or JSON string)
const apiDocsString = JSON.stringify(this.specification)
const etag = `"${Buffer.from(apiDocsString).toString('base64')}"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix indentation and improve ETag generation.

The ETag generation logic has indentation issues and could be more robust.

Apply this diff to fix indentation and improve the ETag generation:

-    // Generate an ETag for the specification (simple hash or JSON string)
-    const apiDocsString = JSON.stringify(this.specification)
-    const etag = `"${Buffer.from(apiDocsString).toString('base64')}"`
+      // Generate an ETag for the specification (simple hash or JSON string)
+      const apiDocsString = JSON.stringify(this.specification)
+      const etag = `"${Buffer.from(apiDocsString).toString('base64')}"`
📝 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
// Generate an ETag for the specification (simple hash or JSON string)
const apiDocsString = JSON.stringify(this.specification)
const etag = `"${Buffer.from(apiDocsString).toString('base64')}"`
// Generate an ETag for the specification (simple hash or JSON string)
const apiDocsString = JSON.stringify(this.specification)
const etag = `"${Buffer.from(apiDocsString).toString('base64')}"`
🧰 Tools
🪛 GitHub Check: build

[failure] 87-87:
Expected indentation of 6 spaces but found 4


[failure] 87-87:
Expected indentation of 6 spaces but found 4


[failure] 86-86:
Expected indentation of 6 spaces but found 4


[failure] 86-86:
Expected indentation of 6 spaces but found 4

🪛 GitHub Actions: Node Lint

[error] 86-86: ESLint: Expected indentation of 6 spaces but found 4. (indent)

🤖 Prompt for AI Agents
In src/api.js around lines 85 to 87, fix the indentation of the ETag generation
code to align properly with surrounding code. Improve the ETag generation by
using a more robust hashing method instead of base64 encoding the JSON string,
such as creating a hash (e.g., SHA-256) of the JSON string to generate a
consistent and reliable ETag value.

Comment thread src/api.js Outdated
@w3nl w3nl requested a review from Copilot July 30, 2025 09:11
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements HTTP caching for the API documentation endpoint to improve performance by reducing server load and bandwidth usage. The changes add ETag support and conditional GET handling to the /v1/api-docs endpoint, allowing clients to cache the documentation and only receive updates when the content changes.

Key Changes:

  • Added ETag generation based on the API specification content
  • Implemented conditional GET support with If-None-Match header handling
  • Added appropriate Cache-Control headers for client-side caching

Reviewed Changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/api.js Implements HTTP caching with ETag generation and conditional GET support for the API docs endpoint
src/server.test.js Adds comprehensive tests to verify caching headers and 304 Not Modified responses

Comment thread src/api.js
Comment on lines +86 to +100
const apiDocsString = JSON.stringify(this.specification)
const etag = `"${Buffer.from(apiDocsString).toString('base64')}"`

router.get('/api-docs', (request, response) => {
// Check for If-None-Match header
const ifNoneMatchHeader = request.headers['if-none-match']
if (ifNoneMatchHeader) {
const etags = ifNoneMatchHeader.split(',').map((tag) => tag.trim())
if (etags.includes('*') || etags.includes(etag)) {
response.status(304).end()
return
}
}
response.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate')
response.setHeader('ETag', etag)
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

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

Generating a base64-encoded ETag from the entire API specification JSON string on every request is inefficient. Consider computing the ETag once during initialization or using a more efficient hash function like crypto.createHash('sha256') to generate a shorter, more performant ETag.

Suggested change
const apiDocsString = JSON.stringify(this.specification)
const etag = `"${Buffer.from(apiDocsString).toString('base64')}"`
router.get('/api-docs', (request, response) => {
// Check for If-None-Match header
const ifNoneMatchHeader = request.headers['if-none-match']
if (ifNoneMatchHeader) {
const etags = ifNoneMatchHeader.split(',').map((tag) => tag.trim())
if (etags.includes('*') || etags.includes(etag)) {
response.status(304).end()
return
}
}
response.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate')
response.setHeader('ETag', etag)
router.get('/api-docs', (request, response) => {
// Check for If-None-Match header
const ifNoneMatchHeader = request.headers['if-none-match']
if (ifNoneMatchHeader) {
const etags = ifNoneMatchHeader.split(',').map((tag) => tag.trim())
if (etags.includes('*') || etags.includes(this.etag)) {
response.status(304).end()
return
}
}
response.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate')
response.setHeader('ETag', this.etag)

Copilot uses AI. Check for mistakes.
Comment thread src/api.js
Comment on lines +86 to +100
const apiDocsString = JSON.stringify(this.specification)
const etag = `"${Buffer.from(apiDocsString).toString('base64')}"`

router.get('/api-docs', (request, response) => {
// Check for If-None-Match header
const ifNoneMatchHeader = request.headers['if-none-match']
if (ifNoneMatchHeader) {
const etags = ifNoneMatchHeader.split(',').map((tag) => tag.trim())
if (etags.includes('*') || etags.includes(etag)) {
response.status(304).end()
return
}
}
response.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate')
response.setHeader('ETag', etag)
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

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

JSON.stringify is called on every request to generate the ETag, which is wasteful. This should be computed once when the specification is loaded or changed, not on every API docs request.

Suggested change
const apiDocsString = JSON.stringify(this.specification)
const etag = `"${Buffer.from(apiDocsString).toString('base64')}"`
router.get('/api-docs', (request, response) => {
// Check for If-None-Match header
const ifNoneMatchHeader = request.headers['if-none-match']
if (ifNoneMatchHeader) {
const etags = ifNoneMatchHeader.split(',').map((tag) => tag.trim())
if (etags.includes('*') || etags.includes(etag)) {
response.status(304).end()
return
}
}
response.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate')
response.setHeader('ETag', etag)
router.get('/api-docs', (request, response) => {
// Check for If-None-Match header
const ifNoneMatchHeader = request.headers['if-none-match']
if (ifNoneMatchHeader) {
const etags = ifNoneMatchHeader.split(',').map((tag) => tag.trim())
if (etags.includes('*') || etags.includes(this.etag)) {
response.status(304).end()
return
}
}
response.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate')
response.setHeader('ETag', this.etag)

Copilot uses AI. Check for mistakes.
Comment thread src/api.js
return
}
}
response.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate')
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

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

[nitpick] The cache duration (3600 seconds) is hardcoded. Consider making this configurable through a constant or configuration parameter to improve maintainability and allow for different caching strategies in different environments.

Suggested change
response.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate')
response.setHeader('Cache-Control', `public, max-age=${this.cacheDuration}, must-revalidate`)

Copilot uses AI. Check for mistakes.
@w3nl w3nl merged commit 7657818 into main Jul 30, 2025
15 checks passed
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.

2 participants