Skip to content

fix: Improve application submission and stability#245

Open
JoachimLK wants to merge 1 commit into
mainfrom
fix/apply-flow-rate-limit-slug-validation
Open

fix: Improve application submission and stability#245
JoachimLK wants to merge 1 commit into
mainfrom
fix/apply-flow-rate-limit-slug-validation

Conversation

@JoachimLK

@JoachimLK JoachimLK commented Jul 26, 2026

Copy link
Copy Markdown
Contributor
  • Add TRUSTED_PROXY environment variable to configure client IP resolution for rate limiting and add documentation for common setups.
  • Add logging for rate limit failures to help diagnose IP resolution issues.
  • Improve job application submission reliability by clamping attribution tracking parameters instead of failing validation.
  • Implement field-level validation errors for public application forms to provide actionable feedback to users.
  • Prevent published job slugs from changing on title edits to avoid breaking existing public links.
  • Add character length constraints to applicant name fields.

Summary

  • What does this PR change?
  • Why is this needed?

PR title must follow Conventional Commits — e.g. feat(jobs): add bulk import or fix: handle null salary. The squash-merged title is what release-please uses to generate the changelog and pick the next version. PRs with non-conventional titles are blocked by CI.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Chore

Validation

  • I tested locally
  • I added/updated relevant documentation
  • I verified multi-tenant scoping and auth behavior for affected API paths

DCO

  • All commits in this PR are signed off (Signed-off-by) via git commit -s

Summary by CodeRabbit

  • New Features

    • Added configurable reverse-proxy trust settings to improve client IP detection and rate limiting.
    • Added clearer rate-limit diagnostics when request thresholds are exceeded.
    • Added inline validation limits and field-specific error messages to application forms.
    • Improved handling of attribution and tracking parameters.
  • Bug Fixes

    • Prevented unintended job slug changes when updating published jobs.
    • Improved proxy-aware rate-limit separation and fallback behavior.
  • Documentation

    • Added reverse-proxy setup guidance and troubleshooting information for self-hosted deployments.

- Add `TRUSTED_PROXY` environment variable to configure client IP
  resolution for rate limiting and add documentation for common setups.
- Add logging for rate limit failures to help diagnose IP resolution
  issues.
- Improve job application submission reliability by clamping attribution
  tracking parameters instead of failing validation.
- Implement field-level validation errors for public application forms
  to provide actionable feedback to users.
- Prevent published job slugs from changing on title edits to avoid
  breaking existing public links.
- Add character length constraints to applicant name fields.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Failed to post review comments.

We encountered an issue with GitHub. Use @coderabbitai full review to retry the review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: af161633-f8f1-4341-87fd-b049f3f54b3b

📥 Commits

Reviewing files that changed from the base of the PR and between 3b051c6 and ef467cd.

📒 Files selected for processing (13)
  • .env.example
  • SELF-HOSTING.md
  • app/components/ApplicationFormBody.vue
  • app/pages/jobs/[slug]/apply.vue
  • server/api/jobs/[id].patch.ts
  • server/api/public/jobs/[slug]/apply.post.ts
  • server/utils/env.ts
  • server/utils/rateLimit.ts
  • server/utils/schemas/publicApplication.ts
  • server/utils/slugify.ts
  • tests/unit/job-slug.test.ts
  • tests/unit/public-application-validation.test.ts
  • tests/unit/rate-limit.test.ts
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: Playwright E2E
  • GitHub Check: Build, typecheck, and test
  • GitHub Check: Simulate new-user setup (setup.sh → docker compose up)
🧰 Additional context used
🪛 OpenGrep (1.25.0)
server/utils/rateLimit.ts

[ERROR] 317-317: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

📝 Walkthrough

Walkthrough

The PR adds configurable trusted-proxy handling for rate limiting, structured public application validation errors, safer attribution parsing, conditional job slug updates, client-side input limits, documentation, and unit-test coverage.

Changes

Proxy-aware rate limiting

Layer / File(s) Summary
Trusted proxy configuration
.env.example, server/utils/env.ts, SELF-HOSTING.md
Documents and validates TRUSTED_PROXY, its supported modes, legacy TRUSTED_PROXY_IP behavior, and reverse-proxy troubleshooting.
Client key resolution
server/utils/rateLimit.ts
Resolves normalized client IPs from configured forwarding headers or sockets, reports the source, and logs rate-limit exceedances with client-key metadata.
Proxy strategy coverage
tests/unit/rate-limit.test.ts
Tests proxy modes, hop counts, fallbacks, address normalization, environment isolation, and separate client buckets.

Public application validation

Layer / File(s) Summary
Application parsing contract
server/utils/schemas/publicApplication.ts
Normalizes attribution fields, applies applicant-field constraints, and exposes structured HTTP 400 validation errors.
Submission validation integration
server/api/public/jobs/[slug]/apply.post.ts, app/pages/jobs/[slug]/apply.vue
Uses shared parsing for multipart and JSON submissions, normalizes repeated query parameters, and maps field errors into the form.
Input constraints and validation coverage
app/components/ApplicationFormBody.vue, tests/unit/public-application-validation.test.ts
Adds input maxlength constraints and tests attribution handling, valid submissions, and field-level errors.

Conditional job slug updates

Layer / File(s) Summary
Slug update policy
server/utils/slugify.ts
Adds conditional slug resolution for custom slugs and draft title changes while preserving published slugs.
Job update integration
server/api/jobs/[id].patch.ts
Applies slug changes only when the resolver returns a new slug.
Slug behavior coverage
tests/unit/job-slug.test.ts
Tests generated slugs, custom overrides, draft retitles, published retitles, blank slugs, and unchanged titles.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ApplicationEndpoint
  participant parsePublicApplication
  participant ApplicationForm
  Client->>ApplicationEndpoint: submit multipart or JSON application
  ApplicationEndpoint->>parsePublicApplication: validate normalized input
  parsePublicApplication-->>ApplicationEndpoint: parsed data or structured 400 error
  ApplicationEndpoint-->>ApplicationForm: return success or field error
  ApplicationForm->>ApplicationForm: display inline validation message
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is conventional, concise, and accurately reflects the PR’s main stability and application-submission fixes.
Description check ✅ Passed The description covers the requested template sections and includes the main changes and rationale at a high level.
Docstring Coverage ✅ Passed Docstring coverage is 80.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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/apply-flow-rate-limit-slug-validation

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.

@JoachimLK JoachimLK changed the title Improve application submission and stability fix: Improve application submission and stability Jul 26, 2026

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

🧹 Nitpick comments (3)
server/utils/env.ts (1)

89-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider validating TRUSTED_PROXY values in the schema.

The field accepts any string, so TRUSTED_PROXY=true or TRUSTED_PROXY=nginx passes startup validation and only surfaces later as a runtime rate_limit.trusted_proxy_invalid log while silently keeping the production cloudflare default — the exact misconfiguration this PR documents as causing spurious 429s. A schema-level constraint turns it into a boot-time failure.

♻️ Proposed schema constraint
-    TRUSTED_PROXY: emptyToUndefined.optional(),
+    TRUSTED_PROXY: emptyToUndefined
+      .pipe(
+        z.string().regex(
+          /^(cloudflare|none|[1-9]|10)$/i,
+          'TRUSTED_PROXY must be "cloudflare", "none", or a hop count 1-10',
+        ),
+      )
+      .optional(),

Note that server/utils/rateLimit.ts intentionally reads process.env directly, so this only adds a startup guard; it does not change resolution behavior.

🤖 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 `@server/utils/env.ts` around lines 89 - 98, Update the TRUSTED_PROXY schema in
the environment configuration to accept only the documented values: cloudflare,
none, or a numeric hop count from 1 through 10. Preserve the existing optional
and empty-to-undefined behavior, while rejecting arbitrary strings such as true
or nginx during startup; leave TRUSTED_PROXY_IP and rateLimit resolution
unchanged.
tests/unit/rate-limit.test.ts (2)

38-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Guard the NODE_ENV restore against an originally-unset value.

process.env.NODE_ENV = undefined stringifies to "undefined" rather than deleting the key, leaking a bogus value to other tests in the worker. Harmless for the === 'production' checks here, but cheap to make correct.

♻️ Proposed tweak
+function restoreNodeEnv() {
+  if (ORIGINAL_ENV.NODE_ENV === undefined) delete process.env.NODE_ENV
+  else process.env.NODE_ENV = ORIGINAL_ENV.NODE_ENV
+}
+
 beforeEach(() => {
   delete process.env.TRUSTED_PROXY
   delete process.env.TRUSTED_PROXY_IP
-  process.env.NODE_ENV = ORIGINAL_ENV.NODE_ENV
+  restoreNodeEnv()
 })
🤖 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 `@tests/unit/rate-limit.test.ts` around lines 38 - 45, Update the NODE_ENV
restoration in beforeEach and afterEach to delete process.env.NODE_ENV when
ORIGINAL_ENV.NODE_ENV was initially unset; otherwise restore its original value.
Preserve restoration of an originally defined NODE_ENV and the existing proxy
cleanup behavior.

218-225: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case where X-Forwarded-For has fewer entries than the configured hop count.

That's the branch where forwardedIpFromRight clamps to index 0 and returns a client-written value — see the comment on server/utils/rateLimit.ts Lines 295-301. A test with TRUSTED_PROXY=2 and a two-entry header where the leftmost is attacker-supplied would pin the intended behavior.

🤖 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 `@tests/unit/rate-limit.test.ts` around lines 218 - 225, Add a unit test
alongside the existing X-Forwarded-For hop-count case that sets TRUSTED_PROXY to
2 and supplies a two-entry header with an attacker-controlled leftmost value,
asserting getClientIp(makeEvent(...)) returns that leftmost value when
forwardedIpFromRight clamps to index 0.
🤖 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 `@app/pages/jobs/`[slug]/apply.vue:
- Around line 258-262: Update the field-level error mapping near the `field`
lookup so server responses naming `coverLetterText` are assigned to
`errors.coverLetter`, while retaining the existing `form.value` mapping for
other fields. Ensure the mapped cover-letter error renders inline rather than
remaining global.

In `@server/utils/rateLimit.ts`:
- Around line 191-205: Prevent invalid explicit TRUSTED_PROXY values from
falling through to the production Cloudflare default. In
server/utils/rateLimit.ts lines 191-205, make the invalid-value path fatal after
logging rate_limit.trusted_proxy_invalid, or explicitly document the intentional
fail-open behavior; in server/utils/env.ts lines 89-98, constrain TRUSTED_PROXY
validation to cloudflare, none, or numeric hop counts 1-10 so invalid
configuration fails during startup.
- Around line 113-122: Update the rate-limit logging in the exceeded-limit path
around resolveClientKey to avoid emitting the raw client IP: apply the
repository’s established privacy-preserving hashing or truncation helper to ip
before assigning client_key, while preserving client_key_source and the
remaining diagnostic fields.
- Around line 295-301: Update forwardedIpFromRight to return undefined when hops
exceeds entries.length instead of clamping the index to zero. Only normalize and
return the selected forwarded entry when the configured hop count is available,
preserving the empty-entry behavior so callers fall back to the socket key and
warning.

In `@server/utils/slugify.ts`:
- Around line 53-54: Update the draft branch in generateJobSlug to regenerate
only when newTitle differs from currentTitle, preserving an existing custom slug
when the submitted title is unchanged. Add a regression test covering an
unchanged draft title without customSlug and verify the custom slug remains
intact.

---

Nitpick comments:
In `@server/utils/env.ts`:
- Around line 89-98: Update the TRUSTED_PROXY schema in the environment
configuration to accept only the documented values: cloudflare, none, or a
numeric hop count from 1 through 10. Preserve the existing optional and
empty-to-undefined behavior, while rejecting arbitrary strings such as true or
nginx during startup; leave TRUSTED_PROXY_IP and rateLimit resolution unchanged.

In `@tests/unit/rate-limit.test.ts`:
- Around line 38-45: Update the NODE_ENV restoration in beforeEach and afterEach
to delete process.env.NODE_ENV when ORIGINAL_ENV.NODE_ENV was initially unset;
otherwise restore its original value. Preserve restoration of an originally
defined NODE_ENV and the existing proxy cleanup behavior.
- Around line 218-225: Add a unit test alongside the existing X-Forwarded-For
hop-count case that sets TRUSTED_PROXY to 2 and supplies a two-entry header with
an attacker-controlled leftmost value, asserting getClientIp(makeEvent(...))
returns that leftmost value when forwardedIpFromRight clamps to index 0.
🪄 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 Plus

Run ID: af161633-f8f1-4341-87fd-b049f3f54b3b

📥 Commits

Reviewing files that changed from the base of the PR and between 3b051c6 and ef467cd.

📒 Files selected for processing (13)
  • .env.example
  • SELF-HOSTING.md
  • app/components/ApplicationFormBody.vue
  • app/pages/jobs/[slug]/apply.vue
  • server/api/jobs/[id].patch.ts
  • server/api/public/jobs/[slug]/apply.post.ts
  • server/utils/env.ts
  • server/utils/rateLimit.ts
  • server/utils/schemas/publicApplication.ts
  • server/utils/slugify.ts
  • tests/unit/job-slug.test.ts
  • tests/unit/public-application-validation.test.ts
  • tests/unit/rate-limit.test.ts

Comment on lines +258 to +262
// Field-level validation failures name their field — show it inline too
const field = err.data?.data?.field
if (status === 400 && typeof field === 'string' && field in form.value) {
errors.value[field] = message
}

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

Map coverLetterText to the displayed cover-letter error key.

The server returns field: "coverLetterText", but this branch only accepts keys in form; cover letter state is separate and local validation uses errors.coverLetter. Server-side cover-letter validation errors therefore remain global instead of rendering beside the field.

Proposed fix
-    const field = err.data?.data?.field
-    if (status === 400 && typeof field === 'string' && field in form.value) {
+    const serverField = err.data?.data?.field
+    const field = serverField === 'coverLetterText' ? 'coverLetter' : serverField
+    if (status === 400 && typeof field === 'string' && (field in form.value || field === 'coverLetter')) {
       errors.value[field] = message
     }
📝 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
// Field-level validation failures name their field — show it inline too
const field = err.data?.data?.field
if (status === 400 && typeof field === 'string' && field in form.value) {
errors.value[field] = message
}
// Field-level validation failures name their field — show it inline too
const serverField = err.data?.data?.field
const field = serverField === 'coverLetterText' ? 'coverLetter' : serverField
if (status === 400 && typeof field === 'string' && (field in form.value || field === 'coverLetter')) {
errors.value[field] = message
}
🤖 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 `@app/pages/jobs/`[slug]/apply.vue around lines 258 - 262, Update the
field-level error mapping near the `field` lookup so server responses naming
`coverLetterText` are assigned to `errors.coverLetter`, while retaining the
existing `form.value` mapping for other fields. Ensure the mapped cover-letter
error renders inline rather than remaining global.

Comment thread server/utils/rateLimit.ts
Comment on lines +113 to +122
// Log the resolved bucket key: a limit that trips for unrelated clients is
// almost always an IP-resolution problem (see resolveClientKey), and that
// is invisible without knowing which key was counted.
logWarn('rate_limit.exceeded', {
client_key: ip,
client_key_source: source,
path: getRequestURL(event).pathname,
limit: maxRequests,
window_ms: windowMs,
})

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 | 🟡 Minor | ⚡ Quick win

Client IP is now written to logs — confirm this fits your retention/redaction posture.

client_key is a raw client IP (personal data under GDPR). Rate-limit abuse diagnostics is a defensible legitimate interest, but this repo already ships GDPR retention machinery, so the log sink should have a bounded retention window — or hash/truncate the key here if logs are long-lived.

🛡️ Optional: log a truncated key instead
       logWarn('rate_limit.exceeded', {
-        client_key: ip,
+        client_key: maskIp(ip),
         client_key_source: source,
🤖 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 `@server/utils/rateLimit.ts` around lines 113 - 122, Update the rate-limit
logging in the exceeded-limit path around resolveClientKey to avoid emitting the
raw client IP: apply the repository’s established privacy-preserving hashing or
truncation helper to ip before assigning client_key, while preserving
client_key_source and the remaining diagnostic fields.

Comment thread server/utils/rateLimit.ts
Comment on lines +191 to +205
logWarn('rate_limit.trusted_proxy_invalid', {
value: trustedProxy,
hint: `Expected "cloudflare", "none", or a hop count 1-${MAX_TRUSTED_HOPS}`,
})
}

// Default: use the socket remote address (cannot be spoofed)
return getRequestIP(event) ?? '0.0.0.0'
if (trustedPeer) return { kind: 'trusted-peer', ip: trustedPeer }

// Hosted reqcore serves every request through Cloudflare → Railway, so the
// socket peer is the platform edge and is identical for all traffic. Keying on
// it would put the entire internet in one bucket — the 6th applicant across
// all orgs would get a 429. Default to the Cloudflare header in production;
// deployments exposed directly to the internet must set TRUSTED_PROXY=none,
// otherwise a client can pick its own bucket by sending CF-Connecting-IP.
if (process.env.NODE_ENV === 'production') return { kind: 'cloudflare' }

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

Unvalidated TRUSTED_PROXY values fail open to cloudflare in production. The root cause is that no layer rejects a bad value: the env schema accepts any string, and the runtime parser warns and then falls through to the production default, which trusts CF-Connecting-IP. On a directly-exposed host where the operator intended none, a typo lets clients choose their own rate-limit bucket.

  • server/utils/rateLimit.ts#L191-L205: after logging rate_limit.trusted_proxy_invalid, don't fall through to the production cloudflare default — either document the fail-open decision explicitly in the comment block or treat an invalid explicit value as fatal.
  • server/utils/env.ts#L89-L98: constrain TRUSTED_PROXY to cloudflare | none | 1-10 so a bad value fails at startup instead of degrading silently at request time.
📍 Affects 2 files
  • server/utils/rateLimit.ts#L191-L205 (this comment)
  • server/utils/env.ts#L89-L98
🤖 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 `@server/utils/rateLimit.ts` around lines 191 - 205, Prevent invalid explicit
TRUSTED_PROXY values from falling through to the production Cloudflare default.
In server/utils/rateLimit.ts lines 191-205, make the invalid-value path fatal
after logging rate_limit.trusted_proxy_invalid, or explicitly document the
intentional fail-open behavior; in server/utils/env.ts lines 89-98, constrain
TRUSTED_PROXY validation to cloudflare, none, or numeric hop counts 1-10 so
invalid configuration fails during startup.

Comment thread server/utils/rateLimit.ts
Comment on lines +295 to +301
function forwardedIpFromRight(event: H3Event, hops: number): string | undefined {
const entries = forwardedEntries(event)
if (entries.length === 0) return undefined

const index = Math.max(0, entries.length - hops)
return normalizeIp(entries[index])
}

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

Clamping the hop index to 0 trusts a client-supplied X-Forwarded-For entry.

When entries.length < hops, Math.max(0, …) silently selects the leftmost entry. With TRUSTED_PROXY=2 but only one proxy actually appending, a client sending X-Forwarded-For: <chosen-ip> produces two entries and index 0 resolves to the value the client chose — self-selected rate-limit bucket. Returning undefined instead falls through to the socket key and emits the existing rate_limit.socket_ip_fallback warning, which also surfaces the misconfiguration.

🛡️ Proposed fix
 function forwardedIpFromRight(event: H3Event, hops: number): string | undefined {
   const entries = forwardedEntries(event)
-  if (entries.length === 0) return undefined
-
-  const index = Math.max(0, entries.length - hops)
-  return normalizeIp(entries[index])
+  // Fewer entries than trusted hops means the chain is shorter than configured;
+  // the leftmost entry would then be client-written, so refuse it.
+  if (entries.length < hops) return undefined
+
+  return normalizeIp(entries[entries.length - hops])
 }
📝 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
function forwardedIpFromRight(event: H3Event, hops: number): string | undefined {
const entries = forwardedEntries(event)
if (entries.length === 0) return undefined
const index = Math.max(0, entries.length - hops)
return normalizeIp(entries[index])
}
function forwardedIpFromRight(event: H3Event, hops: number): string | undefined {
const entries = forwardedEntries(event)
// Fewer entries than trusted hops means the chain is shorter than configured;
// the leftmost entry would then be client-written, so refuse it.
if (entries.length < hops) return undefined
return normalizeIp(entries[entries.length - hops])
}
🤖 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 `@server/utils/rateLimit.ts` around lines 295 - 301, Update
forwardedIpFromRight to return undefined when hops exceeds entries.length
instead of clamping the index to zero. Only normalize and return the selected
forwarded entry when the configured hop count is available, preserving the
empty-entry behavior so callers fall back to the socket key and warning.

Comment thread server/utils/slugify.ts
Comment on lines +53 to +54
if (newTitle && currentStatus === 'draft') {
return generateJobSlug(newTitle, id)

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 | 🟠 Major | ⚡ Quick win

Avoid regenerating a draft slug for an unchanged title.

newTitle is not compared with currentTitle. A draft that already has a custom slug will lose it when an edit submits the same title without customSlug. Only regenerate when the title actually changed; add a same-title regression test.

Proposed fix
-  if (newTitle && currentStatus === 'draft') {
+  if (newTitle && newTitle !== currentTitle && currentStatus === 'draft') {
     return generateJobSlug(newTitle, id)
   }
📝 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 (newTitle && currentStatus === 'draft') {
return generateJobSlug(newTitle, id)
if (newTitle && newTitle !== currentTitle && currentStatus === 'draft') {
return generateJobSlug(newTitle, id)
🤖 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 `@server/utils/slugify.ts` around lines 53 - 54, Update the draft branch in
generateJobSlug to regenerate only when newTitle differs from currentTitle,
preserving an existing custom slug when the submitted title is unchanged. Add a
regression test covering an unchanged draft title without customSlug and verify
the custom slug remains intact.

@railway-app

railway-app Bot commented Jul 26, 2026

Copy link
Copy Markdown

🚅 Deployed to the reqcore-pr-245 environment in applirank

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) Jul 26, 2026 at 1:55 pm

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