Skip to content

Conversation

@asarandi
Copy link
Contributor

@asarandi asarandi commented Nov 1, 2025

fix IdleTimeout parameter parsing to match schema description

fixes #1519

@claude
Copy link
Contributor

claude bot commented Nov 4, 2025

Code Review - PR #1547: Fix IdleTimeout Parameter Parsing

Thank you @asarandi for this important bug fix! This PR addresses a critical issue where the IdleTimeout configuration parameter cannot be parsed from string format as documented in the JSON schema.

Summary

This PR fixes issue #1519 by implementing a custom Duration type with UnmarshalJSON to properly parse Go duration strings (e.g., "5m", "30s", "1h") from JSON configuration files.


✅ Strengths

Code Quality

  • Clean Implementation: The custom Duration type wrapper follows Go best practices for JSON unmarshaling
  • Minimal Changes: Only touches the necessary code to fix the issue
  • Proper Error Handling: Returns parsing errors appropriately
  • Type Safety: Maintains the underlying time.Duration type through embedding

Problem Resolution

  • Fixes Real Bug: Addresses actual user-reported issue where documented string format was not working
  • Matches Schema: Now properly implements what the JSON schema promises (lines 63-74 in config-schema.json)

⚠️ Issues & Concerns

1. Missing MarshalJSON Implementation (Critical)

The PR only implements UnmarshalJSON but not MarshalJSON. This creates an asymmetry issue:

Problem Location: config/confpar/confpar.go:64-72

Why this matters: The HashPlaintextPasswords feature in config/config.go:100-166 reads, modifies, and writes back the configuration file using sjson.Set(). Without MarshalJSON, when the config is written back, the IdleTimeout will be serialized as an integer (nanoseconds) instead of a human-readable string.

Example scenario:

  1. User sets "idle_timeout": "5m" in config
  2. Server starts with hash_plaintext_passwords: true
  3. Config gets rewritten after hashing passwords
  4. idle_timeout becomes 300000000000 (nanoseconds) instead of "5m"

Recommended fix: Add a MarshalJSON method:

func (d Duration) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.Duration.String())
}

2. Inconsistent Duration Handling in Codebase

Problem: AccessesWebhook.Timeout field (line 22) also uses time.Duration with JSON, but doesn't have the same fix applied.

Location: config/confpar/confpar.go:22

type AccessesWebhook struct {
    URL     string            `json:"url"`
    Headers map[string]string `json:"headers"`
    Timeout time.Duration     `json:"timeout"` // Also needs custom unmarshaling!
}

This field likely has the same bug - it won't parse string duration formats from JSON. Consider either:

  • Applying the same fix to AccessesWebhook.Timeout in this PR
  • Creating a follow-up issue to track this

3. Type Export Convention

Minor: The Duration type is exported (uppercase) but may not need to be part of the public API since it's only used internally within Content.

Consideration: Could make it lowercase duration unless external packages need to reference it. However, keeping it exported is fine if you prefer consistency with other types in the package.

4. Missing Test Coverage

Issue: No tests are added for the new functionality. The repository has minimal test coverage already (fs/utils/env_test.go only).

Recommendation: Add a test file config/confpar/confpar_test.go:

func TestDurationUnmarshalJSON(t *testing.T) {
    tests := []struct {
        input    string
        expected time.Duration
        wantErr  bool
    }{
        {`"5m"`, 5 * time.Minute, false},
        {`"30s"`, 30 * time.Second, false},
        {`"1h"`, time.Hour, false},
        {`"0s"`, 0, false},
        {`"invalid"`, 0, true},
    }
    // ... test implementation
}

5. Error Handling Could Be More Explicit

Minor: The UnmarshalJSON implementation uses a short variable declaration that's valid but could be more explicit:

func (d *Duration) UnmarshalJSON(b []byte) (err error) {
    var s string
    if err = json.Unmarshal(b, &s); err == nil {
        d.Duration, err = time.ParseDuration(s)
    }
    return
}

Alternative (more explicit, handles both string and numeric formats):

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        // Fallback: try parsing as number (nanoseconds) for backward compatibility
        var ns int64
        if err2 := json.Unmarshal(b, &ns); err2 != nil {
            return fmt.Errorf("duration must be a string or number: %w", err)
        }
        d.Duration = time.Duration(ns)
        return nil
    }
    
    var err error
    d.Duration, err = time.ParseDuration(s)
    return err
}

This would maintain backward compatibility with users who worked around the bug by using nanosecond integers.


🔒 Security Assessment

  • No security concerns: The fix handles user input appropriately
  • Input validation: time.ParseDuration provides proper validation
  • No injection risks: Duration parsing is type-safe

🚀 Performance Considerations

  • Negligible overhead: Parsing only happens during config load
  • Positive impact: Proper idle timeout handling improves resource management

📋 Recommendations

Must Fix Before Merge:

  1. Add MarshalJSON method to prevent config corruption when HashPlaintextPasswords writes back the config file

Should Consider:

  1. 🔄 Apply same fix to AccessesWebhook.Timeout or create follow-up issue
  2. 🧪 Add unit tests for the new Duration type
  3. 📖 Document the backward compatibility approach (if supporting numeric format)

Before Merge Checklist:

golangci-lint run           # Ensure linting passes
go build -v ./...           # Verify build succeeds
go test -race -v ./...      # Run existing tests

📝 Summary

This is a necessary bug fix that addresses a real user issue. The core implementation is sound, but it needs the MarshalJSON counterpart to be production-ready and prevent config file corruption.

Current Status: ⚠️ Changes Requested

Once MarshalJSON is added, this will be ready to merge. Great work identifying and fixing this issue! 👍


Related Files:

  • config/confpar/confpar.go:68 - IdleTimeout field definition
  • server/server.go:98 - IdleTimeout usage
  • config-schema.json:63-74 - Schema definition

@claude
Copy link
Contributor

claude bot commented Nov 5, 2025

Code Review for PR #1547: Fix IdleTimeout Parameter Parsing

Summary

This PR addresses a mismatch between the JSON schema definition and the Go struct implementation for the idle_timeout configuration parameter. The schema specifies it as a string (Go duration format like "5m", "30s"), but the code was treating it as a numeric duration value.


✅ Positive Aspects

  1. Correct Problem Identification: The PR properly identifies and fixes the schema mismatch. The config-schema.json line 64 defines idle_timeout as type: "string" with examples like "5m", "30s", "1h", but the struct was using time.Duration directly.

  2. Clean Implementation: The custom Duration type with UnmarshalJSON method is a standard Go pattern for handling duration strings in JSON.

  3. Minimal Changes: The fix is focused and doesn't introduce unnecessary modifications.

  4. Proper Usage: The code at server/server.go:98 correctly calls .Seconds() on the Duration field, which works because Duration embeds time.Duration.


⚠️ Concerns & Suggestions

1. Inconsistent Handling of Similar Fields

The AccessesWebhook.Timeout field at config/confpar/confpar.go:22 has the same issue - it's a time.Duration in the struct but would need to be parsed from a string in JSON according to typical configuration patterns. Consider applying the same fix to this field for consistency.

2. Missing Tests

The repository has minimal test coverage (only fs/utils/env_test.go exists). This change would greatly benefit from unit tests to verify:

  • Valid duration strings parse correctly ("5m", "30s", "1h", "0s")
  • Invalid duration strings return appropriate errors
  • Edge cases (empty string, negative values, very large values)
  • Backwards compatibility if numeric values were somehow being used

Example test structure:

func TestDuration_UnmarshalJSON(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    time.Duration
        wantErr bool
    }{
        {"valid duration", `"5m"`, 5 * time.Minute, false},
        {"zero duration", `"0s"`, 0, false},
        {"invalid format", `"invalid"`, 0, true},
    }
    // ... test implementation
}

3. Error Handling Could Be More Explicit

The UnmarshalJSON method silently returns any parsing error. Consider whether you want to wrap errors with context:

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return fmt.Errorf("duration must be a string: %w", err)
    }
    var err error
    d.Duration, err = time.ParseDuration(s)
    if err != nil {
        return fmt.Errorf("invalid duration format: %w", err)
    }
    return nil
}

4. Consider MarshalJSON for Completeness

While not strictly necessary for this use case, implementing MarshalJSON would make the type symmetric and useful for config generation/export:

func (d Duration) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.Duration.String())
}

5. Documentation

Consider adding a comment to the Duration type explaining why it exists:

// Duration wraps time.Duration to allow unmarshaling from JSON strings
// in Go duration format (e.g., "5m", "30s", "1h")
type Duration struct {
    time.Duration
}

🔒 Security Considerations

No security issues identified. Duration parsing is safe and doesn't introduce vulnerabilities.


📊 Performance Considerations

The change has negligible performance impact - duration parsing happens once during configuration loading.


🎯 Code Quality & Style

The code follows Go conventions and aligns with the golangci-lint configuration in the repository. The implementation is idiomatic.


Recommendation

APPROVE with suggestions. The core fix is correct and necessary. I recommend:

  1. Adding unit tests for the new Duration type
  2. Considering whether AccessesWebhook.Timeout needs the same treatment
  3. Optionally improving error messages and adding documentation

The PR can be merged as-is if tests aren't deemed critical, but adding them would significantly improve code quality and prevent regressions.

@asarandi asarandi force-pushed the unmarshal-duration-from-string branch 4 times, most recently from 39831ab to f6e3947 Compare November 6, 2025 03:39
@claude
Copy link
Contributor

claude bot commented Nov 6, 2025

PR Review: Fix IdleTimeout parameter parsing from string

Summary

This PR addresses the issue where idle_timeout and accesses_webhook.timeout configuration parameters could not be parsed from JSON strings in Go duration format. The solution introduces a custom Duration type wrapper with JSON marshaling/unmarshaling support.


Code Quality and Best Practices ✅

Strengths:

  • Clean implementation: The Duration wrapper type follows Go best practices for custom JSON marshaling
  • Minimal changes: Only modified what was necessary - changed two time.Duration fields to Duration and added one accessor (.Duration)
  • Good documentation: The comment on the Duration type clearly explains its purpose
  • Comprehensive tests: The test file covers both marshal and unmarshal operations with multiple cases

Suggestions:

  1. Consider adding godoc examples to help users understand usage
  2. Export the Duration type strategically - confirm this is intentional for external use

Potential Bugs or Issues ⚠️

Minor Issues:

  1. Error handling could be more specific (config/confpar/confpar.go:91-92):

    • The UnmarshalJSON function silently falls through if unmarshal to string fails
    • Consider returning more descriptive errors for invalid JSON values
  2. Potential nil pointer dereference (server/server.go:204):

    • The code accesses s.config.Content.AccessesWebhook.Timeout.Duration without checking if AccessesWebhook is nil
    • However, looking at line 194, there IS a nil check for s.config.Content.AccessesWebhook, so this is actually safe ✅

Performance Considerations ✅

  • No performance concerns: The custom marshaling/unmarshaling adds negligible overhead
  • Embedded struct: Using embedded time.Duration is efficient - no extra allocations
  • String conversion: d.String() in MarshalJSON is the standard Go approach

Security Concerns ✅

No major security issues identified.

Minor consideration:

  • DOS via malformed config: Extremely long duration strings could theoretically be an issue, but Go's time.ParseDuration has reasonable limits and will error on invalid formats

Test Coverage 🟡

Current Coverage:

  • ✅ Marshal to JSON: multiple duration values tested
  • ✅ Unmarshal from JSON: valid and invalid strings tested
  • ✅ Error case: invalid duration format

Missing Coverage:

  1. Integration test: No test showing the full config unmarshaling with idle_timeout and accesses_webhook.timeout as strings
  2. Edge cases: negative durations, zero duration handling, very large duration values

Recommendation: Consider adding an integration test that unmarshals a full config JSON with duration strings to ensure end-to-end functionality.


Alignment with Project Standards ✅

According to CLAUDE.md:

  • ✅ Code quality: Follows golangci-lint standards
  • ✅ Testing: Uses go test -race -v pattern
  • ✅ Line length: Well within 120 character limit
  • ✅ Package structure: Changes properly isolated to config/confpar/ package

Schema Validation 🔍

Important Note: The JSON schema file (config-schema.json) correctly defines idle_timeout as type string (line 64), which matches this PR's implementation. However, I couldn't find a schema definition for accesses_webhook in the schema file - you may want to add that for completeness.


Overall Assessment

Recommendation: APPROVE with minor suggestions

This is a solid, well-tested fix that aligns the code implementation with the documented JSON schema. The custom Duration type is a standard Go pattern for this use case.

Before merging:

  • Consider improving error messages in UnmarshalJSON
  • Consider adding an integration test for full config unmarshaling
  • Optional: Add schema definition for accesses_webhook if it's missing

Great work on fixing this issue and adding comprehensive unit tests!

@claude
Copy link
Contributor

claude bot commented Nov 8, 2025

Code Review

Thank you for this PR! This is a well-implemented fix for the IdleTimeout parameter parsing issue. Here's my detailed review:

Strengths

  1. Correct Solution: The custom Duration type wrapper is the standard Go pattern for unmarshaling duration strings from JSON. This properly addresses the core issue where time.Duration is an int64 alias and unmarshals as a number rather than a string.

  2. Comprehensive Test Coverage: The new config/confpar/confpar_test.go file includes excellent test cases:

    • Tests both marshaling and unmarshaling
    • Covers edge cases (0s, invalid input)
    • Tests various duration formats (seconds, minutes, hours)
    • Includes proper error handling verification
  3. Consistent Implementation: Both MarshalJSON and UnmarshalJSON methods are implemented, ensuring round-trip compatibility.

  4. Backward Compatible: The change correctly updates the usage in server/server.go:204 by accessing .Duration field, maintaining proper type compatibility with context.WithTimeout.

⚠️ Issues Found

Critical: Incomplete Fix

The PR only fixes IdleTimeout but leaves AccessesWebhook.Timeout with the same problem:

// Line 22 in config/confpar/confpar.go - STILL HAS THE BUG
type AccessesWebhook struct {
    URL     string            `json:"url"`
    Headers map[string]string `json:"headers"`
    Timeout time.Duration     `json:"timeout"` // ❌ Should be Duration, not time.Duration
}

This field has the exact same issue - it won't properly unmarshal from JSON strings. While there's no schema definition for accesses_webhook in config-schema.json (which is itself an issue), this timeout field is used in server/server.go:204 and will fail to parse from JSON configuration files.

Recommendation: Apply the same fix to AccessesWebhook.Timeout:

  • Change type from time.Duration to Duration
  • Update the usage in server.go:204 to access .Duration field
  • Add this field to config-schema.json with proper documentation

📝 Minor Suggestions

  1. Test Enhancement: Consider adding a test for unmarshaling from numeric values to ensure it properly fails or handles the legacy case (if any configs have numeric values).

  2. Documentation: The Duration type has a good comment, but consider adding a godoc example showing usage in JSON configuration.

  3. Code Style: The test code formatting is good and follows Go conventions. The empty line spacing in tests is fine per golangci-lint settings (function length limits are 80 lines).

🔒 Security & Performance

  • ✅ No security concerns identified
  • ✅ No performance implications (JSON unmarshaling happens during config load only)
  • ✅ Proper error handling in UnmarshalJSON

📊 Summary

Approve with Changes Required: This PR solves the immediate problem for IdleTimeout, but needs to also fix AccessesWebhook.Timeout to be complete. The implementation pattern is correct and well-tested.

Action Items:

  1. Apply the same Duration type to AccessesWebhook.Timeout field
  2. Update server.go:204 to use .Duration accessor
  3. Consider adding accesses_webhook schema documentation to config-schema.json

Great work on the test coverage and clean implementation! 🎯

@asarandi asarandi force-pushed the unmarshal-duration-from-string branch from a415eb2 to 32d0f7c Compare November 8, 2025 03:19
@claude
Copy link
Contributor

claude bot commented Nov 8, 2025

Code Review - PR #1547

Summary

This PR fixes a configuration parsing issue where IdleTimeout and Timeout (in AccessesWebhook) were incorrectly using time.Duration directly in JSON unmarshaling. The JSON schema specifies these should be strings in Go duration format (e.g., "5m", "30s"), but time.Duration unmarshals as integer nanoseconds, causing a type mismatch.

Positive Aspects

Well-designed solution:

  • The custom Duration wrapper type is a clean, idiomatic Go pattern for handling duration strings in JSON
  • The implementation follows standard library conventions (similar to time.Time JSON marshaling)
  • Backward compatible - existing usages at server/server.go:98 and server/server.go:204 continue to work by accessing the embedded Duration field

Excellent test coverage:

  • Comprehensive unit tests added in confpar_test.go - this is notable since the codebase currently has minimal test coverage (only fs/utils/env_test.go exists)
  • Tests cover both marshal and unmarshal operations
  • Includes edge cases: zero duration, various formats, and error handling for invalid input
  • Clear test structure with table-driven tests

Code quality:

  • Follows the repository's strict linting standards (40+ linters configured)
  • Proper error handling
  • Clean, readable implementation

Issues & Suggestions

1. Missing Documentation (Minor)

The Duration type should have a package-level comment explaining its purpose.

2. Potential Breaking Change (Important)

While the PR aligns with the JSON schema (which specifies "type": "string"), existing configuration files using integer values will break. Consider:

  • Does the project have existing configs in the wild using integer values?
  • Should there be a migration guide or backward compatibility handling?
  • The schema shows examples like "5m", "30s" - confirming this is the intended format

3. Missing Webhook Timeout Schema (Minor)

The AccessesWebhook.Timeout field isn't documented in config-schema.json. This should be added for consistency.

4. Test Coverage Enhancement (Nice to have)

Consider adding tests for:

  • Round-trip marshal/unmarshal to ensure data integrity
  • Integration test loading a sample config file with these fields
  • Testing the actual usage in server.go:98 where .Seconds() is called

Security Considerations

No security concerns identified. The change:

  • Uses standard library time.ParseDuration which has proper validation
  • Includes error handling for malformed input
  • Doesn't introduce any injection risks or privilege escalation

Performance Considerations

Minimal performance impact:

  • String parsing happens only during config load (not hot path)
  • The .Duration field access at server/server.go:98 and server/server.go:204 has zero overhead (direct field access)
  • No memory allocation concerns

Recommendations

Required:

  1. Add godoc comment for the Duration type
  2. Clarify backward compatibility impact - will this break existing configs?

Suggested:
3. Add timeout field to config-schema.json for AccessesWebhook
4. Consider adding integration test with a sample config file

Conclusion

This is a high-quality PR that fixes a legitimate bug where the code didn't match the schema. The implementation is clean, well-tested, and follows Go best practices. The main concern is potential breaking changes to existing configurations.

Recommendation: Approve with minor revisions (documentation improvements). Consider communication about config format changes if this is a breaking change.


Review generated with attention to CLAUDE.md guidelines and Go best practices

@claude
Copy link
Contributor

claude bot commented Nov 8, 2025

Pull Request Review: Fix IdleTimeout parameter parsing from string

Overview

This PR fixes a bug where IdleTimeout and AccessesWebhook.Timeout parameters couldn't be parsed from JSON strings in the configuration file, despite the schema defining them as strings. The fix introduces a custom Duration type wrapper that properly handles JSON marshaling/unmarshaling.

✅ Strengths

1. Clean Solution

  • The custom Duration type wrapper is a standard Go pattern for handling duration strings in JSON
  • Properly embeds time.Duration and delegates to its String() method for marshaling
  • Minimal code changes required - only 3 files modified

2. Good Test Coverage

  • Added comprehensive tests in config/confpar/confpar_test.go (new file)
  • Tests cover both marshaling and unmarshaling
  • Includes edge cases: zero duration, invalid input, various formats (seconds, minutes, hours)
  • Good use of table-driven tests - idiomatic Go style

3. Consistent Application

  • Fixed both IdleTimeout and AccessesWebhook.Timeout fields consistently
  • Updated all usage sites (e.g., server.go:204 now accesses .Duration field)

🔍 Issues & Suggestions

1. Missing Error Handling Context (Minor)
In confpar.go:91, the UnmarshalJSON implementation:

func (d *Duration) UnmarshalJSON(b []byte) (err error) {
    var s string
    if err = json.Unmarshal(b, &s); err == nil {
        d.Duration, err = time.ParseDuration(s)
    }
    return
}

Suggestion: Consider wrapping the error with context for better debugging:

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return fmt.Errorf("duration must be a string: %w", err)
    }
    var err error
    d.Duration, err = time.ParseDuration(s)
    if err != nil {
        return fmt.Errorf("invalid duration format %q: %w", s, err)
    }
    return nil
}

This would help users diagnose config file errors more easily.

2. JSON Schema Alignment
The schema at config-schema.json:63-74 correctly defines idle_timeout as type string, so this fix aligns the Go implementation with the schema. ✅

However, I don't see a schema definition for accesses_webhook.timeout. Consider adding it to the schema for completeness.

3. Test Naming (Nitpick)
The test functions are well-structured, but could follow the repository's conventions more closely. Looking at .golangci.yml, the project uses strict linting. Consider running:

golangci-lint run ./config/confpar/

4. Backward Compatibility
⚠️ Potential Breaking Change: If any existing configs have numeric values for these fields (even though not documented), they will now fail to parse. However:

🔒 Security Considerations

  • No security concerns identified
  • Duration parsing is handled by Go's standard library time.ParseDuration
  • No risk of injection or overflow issues

⚡ Performance Considerations

  • Negligible performance impact
  • JSON unmarshaling happens at config load time (not in hot path)
  • The Duration wrapper adds minimal overhead (one pointer indirection)

📋 Code Quality

  • Follows Go best practices
  • Code is clean and well-documented
  • Fits well with the existing codebase architecture
  • Aligns with .golangci.yml standards (based on initial review)

✅ Recommendations

Required before merge:

  1. Run the full test suite: go test -race -v ./...
  2. Run linting: golangci-lint run
  3. Test with a sample config file that uses the idle_timeout parameter

Optional improvements:

  1. Consider enhanced error messages as suggested above
  2. Add accesses_webhook to the JSON schema if not present
  3. Add a config sample showing idle_timeout usage in /config/samples/

🎯 Verdict

Approval Recommended

This is a solid bug fix that:

The implementation is correct and ready to merge after running the test suite to ensure no regressions.


Review generated with assistance from Claude Code

@asarandi asarandi force-pushed the unmarshal-duration-from-string branch from 32d0f7c to 88601f4 Compare November 8, 2025 21:52
@claude
Copy link
Contributor

claude bot commented Nov 9, 2025

Pull Request Review: Fix IdleTimeout parameter parsing

Thank you for this contribution! This is a solid fix that addresses the issue of parsing duration strings from JSON. Here's my detailed review:

✅ What Works Well

  1. Correct Problem Identification: You've correctly identified that time.Duration doesn't unmarshal from JSON strings by default, requiring a custom wrapper type.

  2. Clean Implementation: The Duration wrapper type with custom MarshalJSON and UnmarshalJSON methods is the idiomatic Go approach for this problem.

  3. Test Coverage: Excellent addition of unit tests for both marshaling and unmarshaling operations, including error cases.

  4. Consistent Fix: The change to server.go:204 correctly accesses the wrapped Duration field.

🔍 Issues and Concerns

1. Incomplete Fix - AccessesWebhook.Timeout Not Updated

The PR only fixes IdleTimeout but leaves AccessesWebhook.Timeout (config/confpar/confpar.go:22) still using raw time.Duration. This creates inconsistency:

  • Current state after PR:

    • IdleTimeoutDuration (custom type, parses from string) ✅
    • AccessesWebhook.Timeouttime.Duration (won't parse from string) ❌
  • Impact: server.go:204 currently uses s.config.Content.AccessesWebhook.Timeout directly as time.Duration, which won't work if the JSON schema expects a string format.

Recommendation: Update AccessesWebhook.Timeout to also use the Duration type for consistency, and update the corresponding usage in server.go:204.

2. Missing JSON Schema for AccessesWebhook

I notice that config-schema.json doesn't include a schema definition for accesses_webhook. While this is a separate issue, it should be addressed to ensure proper validation of the timeout field format.

3. Test Coverage - Edge Cases

The tests are good but could be enhanced with additional edge cases:

  • Very large durations (e.g., "87600h" for 10 years)
  • Negative durations (should they be allowed?)
  • Empty string ""
  • Numeric values instead of strings (type mismatch error handling)

4. Documentation

Consider adding a comment about backward compatibility - existing configs with numeric duration values will break. If there are existing configs in the wild, you might need a more sophisticated unmarshaler that accepts both formats.

📝 Code Quality

Formatting: ✅ Code follows Go conventions
Naming: ✅ Clear and descriptive
Error Handling: ✅ Appropriate (returns parsing errors)
Line Length: ✅ Within 120-character limit per CLAUDE.md

🔒 Security

No security concerns identified. The parsing uses standard library functions and handles errors appropriately.

⚡ Performance

No performance concerns. The JSON parsing overhead is minimal and happens only during configuration loading.

🎯 Recommended Changes

  1. Apply the same Duration wrapper to AccessesWebhook.Timeout:

    type AccessesWebhook struct {
        URL     string            `json:"url"`
        Headers map[string]string `json:"headers"`
        Timeout Duration          `json:"timeout"` // Change from time.Duration
    }
  2. Update server.go:204:

    ctx, cancel := context.WithTimeout(context.Background(), s.config.Content.AccessesWebhook.Timeout.Duration)
  3. Add test for AccessesWebhook timeout parsing if you apply the fix above.

  4. Consider backward compatibility - add a test case or comment about what happens with existing numeric configurations.

Summary

This is a well-implemented fix with good test coverage. The main concern is incompleteness - the same issue exists in another field that should be fixed together. Once AccessesWebhook.Timeout is also updated, this will be ready to merge.

Status: ⚠️ Needs revision to fix AccessesWebhook.Timeout as well for consistency.

@asarandi asarandi force-pushed the unmarshal-duration-from-string branch from 88601f4 to 1011faa Compare November 10, 2025 03:40
@claude
Copy link
Contributor

claude bot commented Nov 11, 2025

Code Review for PR #1547: Fix IdleTimeout Parameter Parsing

Overview

This PR addresses the mismatch between the JSON schema definition (which specifies idle_timeout as a string in Go duration format like "5m", "30s") and the actual Go struct field type (time.Duration). The solution introduces a custom Duration wrapper type with proper JSON marshaling/unmarshaling.


✅ Positive Aspects

  1. Correct Problem Identification: The PR correctly identifies that time.Duration doesn't naturally unmarshal from JSON strings, which caused a mismatch with the schema definition.

  2. Clean Solution: The custom Duration type wrapper is a standard Go pattern for handling this exact scenario.

  3. Good Test Coverage: The new confpar_test.go file includes comprehensive tests for both marshaling and unmarshaling, including edge cases and error conditions.

  4. Proper Error Handling: The UnmarshalJSON method correctly propagates errors from time.ParseDuration.

  5. Consistent Application: The fix is applied to both IdleTimeout fields where it's needed.


⚠️ Issues & Concerns

1. Incomplete Fix: Missing AccessesWebhook.Timeout

The PR updates IdleTimeout to use the custom Duration type but only partially updates AccessesWebhook.Timeout.

Current state in PR:

  • ✅ Line 22: Changed to Timeout Duration
  • ✅ Line 68: Changed to IdleTimeout Duration
  • ✅ Line 204 in server.go: Updated to .Timeout.Duration
  • Missing: No JSON schema definition for accesses_webhook.timeout field

Issue: The AccessesWebhook.Timeout field is changed to use the custom Duration type, but there's no corresponding JSON schema definition. This means:

  • Users have no guidance on the expected format
  • No examples are provided in the schema
  • The field may not validate properly

Recommendation: Add a schema definition for the accesses_webhook object in config-schema.json, including the timeout field with proper type, description, and examples.

2. Potential Breaking Change

While this change makes the code match the schema, it could be a breaking change if:

  • Users are currently using numeric values (nanoseconds) in their JSON configs
  • The old code was accepting both formats

Current behavior: time.Duration in JSON typically unmarshals from integers (nanoseconds).
New behavior: Only accepts string format like "5m", "30s".

Recommendation:

  • Document this as a breaking change in the PR description
  • Consider backward compatibility by checking if the JSON value is a number first, then falling back to string parsing
  • Add a migration note for users

Example backward-compatible implementation:

func (d *Duration) UnmarshalJSON(b []byte) error {
    // Try parsing as string first (new format)
    var s string
    if err := json.Unmarshal(b, &s); err == nil {
        var parseErr error
        d.Duration, parseErr = time.ParseDuration(s)
        return parseErr
    }
    
    // Fall back to numeric format (old format, nanoseconds)
    var ns int64
    if err := json.Unmarshal(b, &ns); err == nil {
        d.Duration = time.Duration(ns)
        return nil
    }
    
    return fmt.Errorf("invalid duration format")
}

3. Missing Test for Backward Compatibility

If backward compatibility is added, tests should cover:

  • Parsing integer values (old format)
  • Parsing string values (new format)
  • Round-trip marshal/unmarshal consistency

4. Code Quality - Minor Issues

In confpar_test.go:20:

b, err := json.Marshal(&tt.input)

The & is unnecessary since tt.input is already a Duration struct, not a pointer. Should be:

b, err := json.Marshal(tt.input)

Test naming: Consider more descriptive test names like TestDurationMarshalJSON_VariousFormats or grouping with subtests using t.Run() for better failure reporting.

5. Documentation

Missing:

  • No update to documentation/README about the duration format
  • No migration guide for existing users
  • The JSON schema needs updating for consistency

Recommendation: Add a comment in the JSON schema examples or update the repository documentation.


🔒 Security Considerations

No security concerns identified:

  • Input validation is handled by time.ParseDuration which is a standard library function
  • No user-controlled code execution
  • Proper error handling prevents panics

🚀 Performance Considerations

Minimal performance impact:

  • String parsing via time.ParseDuration is efficient
  • No unnecessary allocations
  • Proper reuse of the embedded time.Duration type

📋 Recommendations Summary

Must Fix:

  1. Add JSON schema for accesses_webhook including the timeout field
  2. Consider backward compatibility for existing configs using numeric durations
  3. Fix unnecessary pointer in test at line 20

Should Fix:

  1. Document the breaking change if backward compatibility is not added
  2. Add more test cases for error scenarios (empty string, malformed input)
  3. Use subtests with t.Run() for better test organization

Nice to Have:

  1. Update documentation/README with duration format examples
  2. Add integration test with actual JSON config file parsing

🎯 Overall Assessment

Quality: Good - The core fix is sound and well-tested
Completeness: 7/10 - Missing schema update and potential backward compatibility issues
Risk: Medium - Could break existing configurations

This is a valuable fix that aligns the code with the documented schema. With the recommended changes, it would be production-ready.


Reviewed with 🤖 Claude Code

@asarandi asarandi force-pushed the unmarshal-duration-from-string branch from 1011faa to 1b9e7a2 Compare November 12, 2025 00:38
@claude
Copy link
Contributor

claude bot commented Nov 12, 2025

Pull Request Review

Thank you for this PR! This is a well-structured fix that properly addresses the mismatch between the JSON schema and the actual Go struct parsing.

Strengths

  1. Problem correctly identified: The schema defines idle_timeout and timeout as strings (Go duration format like "5m", "30s"), but time.Duration doesn't natively unmarshal from JSON strings - it expects numeric nanoseconds.

  2. Clean solution: The custom Duration wrapper type with UnmarshalJSON/MarshalJSON methods is a standard Go pattern for handling duration strings in JSON.

  3. Good test coverage: The new test file confpar_test.go provides comprehensive coverage:

    • Tests both marshal and unmarshal operations
    • Covers various duration formats (seconds, minutes, hours)
    • Includes error case handling (invalid input)
    • Tests edge cases (zero duration)
  4. Minimal changes: The PR only touches what's necessary and maintains backward compatibility.

  5. Proper usage update: The change to server/server.go:204 correctly accesses the underlying .Duration field.

🔍 Observations & Suggestions

1. Consistency in accessing the wrapped Duration

In server/server.go:98, you access conf.IdleTimeout.Seconds() directly. Since IdleTimeout is now type Duration (not time.Duration), this works because the embedded time.Duration is promoted. However, for clarity and consistency, consider:

// Current (works due to embedding)
IdleTimeout: int(conf.IdleTimeout.Seconds()),

// More explicit (optional, but clearer)
IdleTimeout: int(conf.IdleTimeout.Duration.Seconds()),

The current approach is fine since Go promotes embedded fields, but being explicit might help future maintainers.

2. Consider adding godoc comments

The Duration type and its methods could benefit from exported documentation:

// Duration wraps time.Duration to allow unmarshaling from JSON strings
// in Go duration format (e.g., "5m", "30s", "1h").
//
// When marshaling to JSON, it produces a string representation.
// When unmarshaling from JSON, it accepts Go duration format strings.
type Duration struct {
    time.Duration
}

// MarshalJSON implements json.Marshaler, encoding the duration as a string.
func (d *Duration) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.String())
}

// UnmarshalJSON implements json.Unmarshaler, parsing duration strings like "5m" or "1h".
func (d *Duration) UnmarshalJSON(b []byte) (err error) {
    // ... existing implementation
}

3. Test naming convention

The test functions follow good Go conventions. You might consider adding a test for the round-trip (marshal then unmarshal) to ensure symmetry:

func TestDurationRoundTrip(t *testing.T) {
    original := Duration{5 * time.Minute}
    
    // Marshal
    data, err := json.Marshal(original)
    if err != nil {
        t.Fatalf("marshal failed: %v", err)
    }
    
    // Unmarshal
    var decoded Duration
    if err := json.Unmarshal(data, &decoded); err != nil {
        t.Fatalf("unmarshal failed: %v", err)
    }
    
    // Compare
    if decoded.Duration != original.Duration {
        t.Fatalf("round-trip failed: got %v, want %v", decoded.Duration, original.Duration)
    }
}

4. Error handling edge case

The UnmarshalJSON implementation is clean, but consider what happens if the JSON contains a number instead of a string (for backward compatibility if any configs have numeric values). Current behavior will fail, which is probably correct - just worth documenting.

🎯 Code Quality Assessment

  • Style: Follows Go conventions ✅
  • Error handling: Appropriate ✅
  • Test coverage: Good (new tests added for new functionality) ✅
  • Performance: No concerns (minimal overhead) ✅
  • Security: No concerns ✅

📋 Checklist Before Merge

  • Ensure go test -race -v ./... passes
  • Ensure golangci-lint run passes
  • Consider adding a sample config file demonstrating the duration format
  • Update any relevant documentation about configuration format (if needed)

Recommendation

Approve with minor suggestions. The core implementation is solid and solves the reported issue correctly. The suggestions above are optional improvements that could be addressed in this PR or future work.

Great work on the clean implementation and comprehensive tests! 👍


Review generated with assistance from Claude Code

@asarandi asarandi force-pushed the unmarshal-duration-from-string branch from 1b9e7a2 to 8c1f425 Compare November 14, 2025 20:35
@claude
Copy link
Contributor

claude bot commented Nov 15, 2025

Pull Request Review: Fix IdleTimeout Parameter Parsing

Thank you for this contribution! This PR addresses an important issue where duration fields in the JSON configuration weren't properly parsing from string format (e.g., "5m", "30s") as documented in the schema.

✅ Strengths

1. Core Implementation is Sound

  • The custom Duration wrapper type correctly implements MarshalJSON and UnmarshalJSON to handle Go duration string format
  • Properly embeds time.Duration to inherit all its methods and functionality
  • The fix aligns the code with the JSON schema documentation (config-schema.json:63-73) which specifies duration strings

2. Excellent Test Coverage

  • Comprehensive test suite in confpar_test.go covering both marshaling and unmarshaling
  • Tests edge cases including zero duration and error handling
  • Good test structure with table-driven tests

3. Consistent Application

  • Applied to both IdleTimeout and AccessesWebhook.Timeout fields
  • Updated usage in server/server.go:204 to access the embedded .Duration field

🔍 Issues & Suggestions

1. Missing Schema Update (Important)
The config-schema.json should be updated to document the accesses_webhook.timeout field with type string and duration format examples.

2. Backwards Compatibility Concern
The current implementation ONLY accepts string format. If any existing configurations use numeric format (nanoseconds as integers), they will break. Consider supporting both formats in UnmarshalJSON - try string first, fall back to int64 for backwards compatibility.

3. Code Quality (Minor)
In confpar_test.go:20,45, the err variable assignment could be inlined for cleaner code.

4. Duration Field Access
Good catch updating server.go:98 and server.go:204 to access .Duration from the wrapper type.

🔒 Security Considerations

  • No security concerns identified
  • Duration parsing uses standard library time.ParseDuration which has built-in bounds checking
  • No risk of injection or overflow attacks

⚡ Performance Considerations

  • Minimal performance impact - unmarshaling happens only at config load time
  • The wrapper type adds no runtime overhead (embedded struct)

📋 Recommendations

Must Have:

  1. Add backwards compatibility support for numeric duration values
  2. Update config-schema.json to document accesses_webhook.timeout

Nice to Have:

  1. Add example configuration file showing duration usage
  2. Consider adding integration test that loads a full JSON config with duration fields
  3. Document the breaking change in PR description or migration notes

🎯 Overall Assessment

This is a well-implemented fix that addresses a real discrepancy between the schema and implementation. The test coverage is excellent. With the backwards compatibility improvement and schema documentation, this will be ready to merge.

Recommendation: Request changes for backwards compatibility and schema update.


Review performed following repository conventions from CLAUDE.md

@asarandi asarandi force-pushed the unmarshal-duration-from-string branch from 8c1f425 to 4644820 Compare November 19, 2025 04:43
@claude
Copy link
Contributor

claude bot commented Nov 19, 2025

Code Review - PR #1547: Fix IdleTimeout Parameter Parsing

Thank you @asarandi for this PR! I've reviewed the changes addressing the configuration parsing issue. Here's my detailed analysis:


📋 Summary

This PR fixes a bug where IdleTimeout and AccessesWebhook.Timeout configuration parameters couldn't be parsed from JSON strings in Go duration format (e.g., "5m", "30s"), despite the JSON schema defining them as strings. The solution introduces a custom Duration wrapper type with proper JSON marshaling support.

Files Changed:

  • config/confpar/confpar.go - Added custom Duration type with JSON marshaling methods
  • config/confpar/confpar_test.go - New comprehensive test file
  • server/server.go - Updated to access .Duration field

✅ Strengths

1. Correct Implementation Pattern

The custom Duration type wrapper is the idiomatic Go solution for this problem:

  • Embeds time.Duration for natural usage
  • Implements both MarshalJSON and UnmarshalJSON for round-trip compatibility
  • Clean, minimal implementation (only ~15 lines of code)

2. Excellent Test Coverage 🎯

This is particularly noteworthy given the repository's limited test coverage:

  • 59 new test lines covering both marshal and unmarshal operations
  • Edge cases included: zero duration, various formats, invalid input
  • Clear table-driven test structure following Go best practices
  • Tests verify error handling for malformed input

3. Comprehensive Fix

Both affected fields are properly addressed:

  • Content.IdleTimeout (line 68)
  • AccessesWebhook.Timeout (line 22)
  • Usage site updated correctly (server.go:204)

4. Schema Alignment

The fix properly aligns code with the documented schema (config-schema.json:63-74) which specifies:

"idle_timeout": {
    "type": "string",
    "description": "Duration after which idle client connections will be closed. Use Go duration format..."
}

🔍 Code Quality Assessment

Marshaling Implementation (confpar.go:87-89)

func (d *Duration) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.String())
}

Excellent - Uses time.Duration.String() which produces canonical Go duration format

Unmarshaling Implementation (confpar.go:91-97)

func (d *Duration) UnmarshalJSON(b []byte) (err error) {
    var s string
    if err = json.Unmarshal(b, &s); err == nil {
        d.Duration, err = time.ParseDuration(s)
    }
    return
}

Good - Proper error propagation, uses standard library time.ParseDuration

Minor suggestion: Could add error context for better debugging:

if err != nil {
    return fmt.Errorf("invalid duration format: %w", err)
}

Test Quality (confpar_test.go)

Excellent test structure:

  • Clear test case naming
  • Comprehensive coverage of valid and invalid inputs
  • Proper use of bytes.Equal for comparison
  • Good error case validation with wantErr flag

🔒 Security Considerations

No security concerns identified:

  • Uses standard library time.ParseDuration which has built-in validation
  • Rejects invalid formats appropriately
  • No injection vectors or privilege escalation risks
  • Input validation is type-safe

⚡ Performance Considerations

Negligible performance impact:

  • Unmarshaling happens only during configuration load (once at startup)
  • The embedded Duration field access has zero overhead (direct memory access)
  • No additional allocations beyond standard JSON parsing
  • String conversion in MarshalJSON is standard library code

🧪 Test Coverage Analysis

Current Coverage:

  • ✅ MarshalJSON: 4 test cases (0s, 31s, 5m0s, 1h0m0s)
  • ✅ UnmarshalJSON: 5 test cases (valid formats + error case)
  • ✅ Error handling: Invalid duration string properly tested

What's tested well:

  • Round-trip compatibility (marshal → unmarshal)
  • Various duration magnitudes (seconds, minutes, hours)
  • Zero duration edge case
  • Invalid input rejection

Nice to have (optional):

  • Integration test loading full config JSON with these fields
  • Negative duration handling (though time.ParseDuration handles this)

📐 Adherence to Project Standards

Per CLAUDE.md and .golangci.yml:

  • Line length: All lines well under 120 char limit
  • Function length: Both methods well under 80 lines / 40 statements
  • Code formatting: Follows gofmt / goimports standards
  • Package structure: Changes properly isolated to confpar package
  • Error handling: Proper error propagation
  • Documentation: Type comment explains purpose clearly

🐛 Potential Issues

None identified. The implementation is solid.

The author's comment about tests being "arguably unnecessary" is incorrect - these tests:

  1. Document the expected behavior
  2. Prevent regression if the code is modified
  3. Serve as examples for future contributors
  4. Are minimal overhead (59 lines) for significant value

I strongly recommend keeping the tests.


🎯 Recommendations

Before Merge:

  1. ✅ Run linting: golangci-lint run
  2. ✅ Verify build: go build -v ./...
  3. ✅ Run tests: go test -race -v ./...

Optional Enhancements:

  1. Consider adding error context wrapping in UnmarshalJSON for better debugging
  2. Consider adding timeout field documentation to config-schema.json for AccessesWebhook

✅ Final Recommendation

APPROVE - Ready to Merge 👍

This is a high-quality PR that:

The implementation is clean, well-tested, and ready for production. Great work! 🎉


Note: I apologize for any confusion from previous automated reviews that may have referenced outdated code states. This review is based on the actual current state of the PR.

@claude
Copy link
Contributor

claude bot commented Nov 20, 2025

Code Review for PR #1547

Summary

This PR fixes the IdleTimeout parameter parsing to properly accept string duration values (e.g., "5m", "30s") as specified in the JSON schema, fixing issue #1519.

✅ Positive Aspects

1. Well-Designed Solution

  • The custom Duration type wrapper is a clean, idiomatic Go approach to handle JSON unmarshaling of duration strings
  • Properly implements both MarshalJSON and UnmarshalJSON for bidirectional conversion
  • Maintains backward compatibility by embedding time.Duration

2. Excellent Test Coverage

  • New test file config/confpar/confpar_test.go provides comprehensive coverage with 59 lines of tests
  • Tests both marshaling and unmarshaling directions
  • Includes edge cases (0s duration, invalid input)
  • Tests multiple duration formats (seconds, minutes, hours)

3. Correct Implementation

  • Properly updates both usage sites: IdleTimeout (line 68) and AccessesWebhook.Timeout (line 22)
  • Correctly accesses the embedded Duration field in server/server.go:98 and server/server.go:204 (though see note below)
  • Aligns with existing JSON schema documentation

🔍 Issues & Concerns

1. Incomplete Fix - AccessesWebhook.Timeout Not Updated in server.go
The PR updates AccessesWebhook.Timeout type in confpar.go:22 but does NOT update the usage in server/server.go:204.

Current (main branch):

ctx, cancel := context.WithTimeout(context.Background(), s.config.Content.AccessesWebhook.Timeout)

Should be:

ctx, cancel := context.WithTimeout(context.Background(), s.config.Content.AccessesWebhook.Timeout.Duration)

This will cause a compilation error because context.WithTimeout expects time.Duration, not the custom Duration type. While the diff shows this change, it appears to be missing from the current codebase based on my grep results showing line 204 still uses the old code.

2. Missing Test for AccessesWebhook.Timeout
While IdleTimeout is well-tested through the new unit tests, there's no test verifying that AccessesWebhook.Timeout properly unmarshals from JSON strings. Consider adding a test that unmarshals a complete Content struct with an AccessesWebhook to ensure end-to-end functionality.

💡 Recommendations

High Priority:

  1. ✅ Verify that server/server.go:204 is updated to use .Duration field accessor
  2. Add integration test demonstrating full JSON config unmarshaling with both timeout fields

Low Priority (Nice to Have):

  1. Consider adding a godoc comment to the Duration type explaining its purpose
  2. The test could validate that re-marshaling produces equivalent JSON (round-trip testing)
  3. Consider whether the AccessesWebhook.Timeout field should also be documented in the JSON schema (it doesn't appear to be currently)

📊 Code Quality Assessment

  • ✅ Follows Go conventions and idioms
  • ✅ No security concerns
  • ✅ No performance issues
  • ✅ Consistent with repository's golangci-lint standards
  • ✅ Proper error handling in UnmarshalJSON
  • ✅ No backwards compatibility issues

Verdict

This is a well-implemented fix with excellent test coverage. However, please verify the compilation succeeds and that both server/server.go:98 and server/server.go:204 properly access the .Duration field. Once confirmed, this is ready to merge.


Great work on the comprehensive tests! 👍

@claude
Copy link
Contributor

claude bot commented Nov 20, 2025

Pull Request Review: fix(conf): parse IdleTimeout parameter from string

Summary

This PR successfully fixes the issue where IdleTimeout and AccessesWebhook.Timeout couldn't be parsed from JSON strings in Go duration format (e.g., "5m", "30s"). The solution introduces a custom Duration wrapper type that implements JSON marshaling/unmarshaling.


✅ Strengths

1. Correct Problem Identification
The PR properly addresses the mismatch between the JSON schema (which specifies idle_timeout as a string with examples like "5m") and the Go code (which expected time.Duration to unmarshal directly from strings).

2. Clean Implementation

  • The Duration wrapper type in config/confpar/confpar.go:79-97 is well-designed and follows Go best practices
  • Both MarshalJSON and UnmarshalJSON are implemented for bidirectional compatibility
  • The embedded time.Duration allows transparent usage of all duration methods

3. Comprehensive Test Coverage
The new test file config/confpar/confpar_test.go includes:

  • Marshaling tests with various durations (0s, 31s, 5m, 1h)
  • Unmarshaling tests with valid inputs
  • Error handling test for invalid input
  • Good coverage of edge cases

4. Consistent Application
The fix is applied to both duration fields in the config:

  • Content.IdleTimeout (line 68)
  • AccessesWebhook.Timeout (line 22)
  • Both usages in server/server.go are updated (lines 98 and 204)

🔍 Issues & Concerns

1. Breaking Change - AccessesWebhook.Timeout ⚠️
The change to AccessesWebhook.Timeout at config/confpar/confpar.go:22 alters how this field is accessed:

Before:

ctx, cancel := context.WithTimeout(context.Background(), s.config.Content.AccessesWebhook.Timeout)

After (from PR):

ctx, cancel := context.WithTimeout(context.Background(), s.config.Content.AccessesWebhook.Timeout.Duration)

However, I notice the PR diff shows the change at server/server.go:204, but I need to verify this is consistent. The fix correctly accesses .Duration to unwrap the embedded time.Duration.

2. JSON Schema Not Updated
The config-schema.json file does not appear to be updated in this PR. The AccessesWebhook.Timeout field should also have its schema updated to match the pattern used for idle_timeout:

  • Change type from integer or number to string
  • Add description about Go duration format
  • Add examples like "5m", "30s", "1h"

3. Potential Test Gap
Consider adding a full integration test that:

  • Loads a JSON config file with duration strings
  • Verifies the values are correctly parsed
  • Ensures the values work correctly when used (e.g., in context.WithTimeout)

4. Documentation
The AccessesWebhook.Timeout field change should be mentioned in the PR description or commit message, as it affects existing configurations.


🔒 Security Considerations

✅ No security issues identified. The duration parsing uses Go's standard time.ParseDuration, which is safe and well-tested.


⚡ Performance Considerations

✅ Negligible performance impact:

  • JSON unmarshaling happens only at config load time
  • The overhead of the custom unmarshal method is minimal
  • No runtime performance degradation

📋 Code Quality

Adherence to Repository Standards:
✅ Follows golangci-lint rules from .golangci.yml
✅ Proper package documentation
✅ Consistent naming conventions
✅ Line length within 120 character limit

Suggestions:

  1. Consider adding godoc comments to the Duration type explaining its purpose:
// Duration wraps time.Duration to support JSON unmarshaling from Go duration
// format strings (e.g., "5m", "30s", "1h"). This allows configuration files
// to use human-readable duration strings instead of raw integers.
type Duration struct {
    time.Duration
}
  1. The test function names could be more descriptive per Go conventions:
    • TestDurationMarshalJSONTestDuration_MarshalJSON
    • TestDurationUnmarshalJSONTestDuration_UnmarshalJSON

📝 Recommendations

Must Address:

  1. Update config-schema.json to reflect the string type for accesses_webhook.timeout field (if this field is documented in the schema)
  2. Verify backward compatibility: Ensure existing configs with AccessesWebhook still work correctly

Should Consider:
3. Add a migration note or changelog entry about the config format change
4. Consider adding an example config file demonstrating the new duration string format
5. Enhance godoc comments for the Duration type

Nice to Have:
6. Integration test that loads a sample config with duration strings
7. Add test case for zero value: Duration{}


✅ Conclusion

This is a well-implemented fix that correctly addresses issue #1519. The code quality is high, tests are comprehensive, and the solution is clean. With minor updates to documentation and the JSON schema, this PR will be ready to merge.

Recommendation: Approve with minor revisions (schema update and documentation improvements)


Great work on this fix! The custom Duration type is an elegant solution that maintains backward compatibility while enabling the more user-friendly string format. 👍

@asarandi asarandi force-pushed the unmarshal-duration-from-string branch 4 times, most recently from cabf270 to 8a1d5e9 Compare November 20, 2025 05:43
@claude
Copy link
Contributor

claude bot commented Nov 20, 2025

Code Review: PR #1547 - Fix IdleTimeout Parameter Parsing

Thank you for this PR! This is a well-thought-out fix that addresses a real issue with duration parsing from JSON configuration. Here's my comprehensive review:

Strengths

  1. Correct Problem Identification: The PR correctly identifies that idle_timeout in the JSON schema is defined as a string (e.g., "5m", "30s") but the Go code was using time.Duration directly, which doesn't unmarshal from strings properly.

  2. Good Implementation Pattern: The custom Duration type wrapper is a clean, idiomatic Go solution that:

    • Embeds time.Duration for easy access to all duration methods
    • Implements both MarshalJSON and UnmarshalJSON for bidirectional conversion
    • Uses time.ParseDuration for proper parsing
  3. Excellent Test Coverage: The test file (confpar_test.go) is well-structured with:

    • Comprehensive test cases covering various duration formats
    • Error case handling
    • Clear test organization
  4. Consistent Application: The PR correctly applies the fix to both IdleTimeout and AccessesWebhook.Timeout fields.

🐛 Critical Issue Found

Missing Update in server/server.go:98

The PR changes IdleTimeout from time.Duration to Duration type, but doesn't update line 98 in server/server.go:

IdleTimeout: int(conf.IdleTimeout.Seconds()),

Since Duration wraps time.Duration, this line needs to be updated to:

IdleTimeout: int(conf.IdleTimeout.Duration.Seconds()),

Without this change, the code will fail to compile because Duration (the struct) doesn't have a Seconds() method - only the embedded time.Duration field does.

📝 Minor Suggestions

  1. Consider Adding godoc Comments: The Duration type and its methods could benefit from godoc comments explaining their purpose:

    // Duration wraps time.Duration to allow unmarshaling from JSON strings
    // in Go duration format (e.g., "5m", "30s", "1h").
    type Duration struct {
        time.Duration
    }
    
    // MarshalJSON converts the Duration to a JSON string.
    func (d *Duration) MarshalJSON() ([]byte, error) {
        return json.Marshal(d.String())
    }
    
    // UnmarshalJSON parses a duration string from JSON.
    func (d *Duration) UnmarshalJSON(b []byte) error {
        // implementation
    }
  2. Error Handling in UnmarshalJSON: The current implementation is correct, but could be slightly more explicit:

    func (d *Duration) UnmarshalJSON(b []byte) error {
        var s string
        if err := json.Unmarshal(b, &s); err != nil {
            return err
        }
        var err error
        d.Duration, err = time.ParseDuration(s)
        return err
    }

    This is functionally equivalent but some might find it more readable.

  3. Test Case Naming: Consider using table-driven test naming for better failure messages:

    t.Run(item.input, func(t *testing.T) {
        // test code
    })

🔒 Security Considerations

✅ No security concerns identified. The changes:

  • Don't introduce any injection vulnerabilities
  • Use standard library parsing (time.ParseDuration) which is safe
  • Don't expose any sensitive information

📊 Performance Considerations

✅ Performance impact is negligible:

  • JSON unmarshaling happens once at configuration load time
  • The custom type adds minimal overhead
  • No performance-critical paths affected

🎯 Recommendations

Must Fix:

  • Update server/server.go:98 to use conf.IdleTimeout.Duration.Seconds()

Nice to Have:

  • Add godoc comments to the Duration type and methods
  • Consider running golangci-lint run to ensure code quality standards

📋 Testing Checklist

Before merging, please verify:

  • The code compiles successfully with all changes
  • Existing tests pass: go test -race -v ./...
  • New tests pass: go test -v ./config/confpar/
  • Configuration file with string durations (e.g., "idle_timeout": "5m") loads correctly
  • The FTP server respects the idle timeout setting

Overall, this is a solid PR that addresses a real issue. Once the missing server.go update is added, this will be ready to merge. Great work on the comprehensive tests! 👍

@asarandi asarandi force-pushed the unmarshal-duration-from-string branch from f374cd9 to e42ceda Compare November 20, 2025 06:11
@claude
Copy link
Contributor

claude bot commented Nov 20, 2025

Code Review for PR #1547: fix(conf): parse IdleTimeout parameter from string

Overview

This PR fixes issue #1519 by implementing proper JSON unmarshaling for duration fields (IdleTimeout and AccessesWebhook.Timeout) from Go duration strings (e.g., "5m", "30s", "1h"). The implementation introduces a custom Duration wrapper type with JSON marshaling/unmarshaling support.


✅ Strengths

1. Clean Implementation

  • The custom Duration type with embedded time.Duration is a solid, idiomatic Go pattern
  • Minimal changes to existing code - only type changes and one .Duration accessor
  • Good separation of concerns with marshaling logic isolated in the wrapper type

2. Excellent Test Coverage

  • New test file confpar_test.go provides comprehensive coverage for both marshal and unmarshal operations
  • Tests include edge cases (0s, various time units, invalid input)
  • Error handling is properly tested

3. Schema Consistency

  • Changes align with existing JSON schema documentation that specifies string format for durations
  • Maintains backward compatibility with the schema

🔍 Issues & Suggestions

1. Critical: Missing Test Coverage for Backward Compatibility

The most important consideration is backward compatibility. While the new code parses duration strings correctly, we need to verify it handles numeric JSON values (if they were previously supported). The time.Duration type can unmarshal from JSON numbers (nanoseconds).

2. Minor: Incomplete Coverage of Edge Cases

  • Zero values: empty string, null JSON values
  • Negative durations: "-5m" (valid in Go)

3. Documentation: Missing Godoc Comments
The new Duration type and its methods lack documentation comments.

4. Code Quality: Unmarshaling Could Be More Robust

  • No explicit handling of JSON null
  • No handling of numeric inputs (potential breaking change)

5. Integration Testing
While unit tests are present, there's no integration test verifying the entire config unmarshaling flow works with the new Duration type.


🔒 Security Considerations

No security issues identified. The change is type-safe and doesn't introduce command injection, buffer overflows, or unvalidated input handling issues. The time.ParseDuration function is from the standard library and is safe to use with user input.


⚡ Performance Considerations

Negligible performance impact. The marshaling/unmarshaling overhead is minimal - only occurs during config loading at startup.


📋 Summary

Overall Assessment: Good implementation with solid fundamentals. The core logic is sound, but needs attention to backward compatibility and edge case handling.

Required Before Merge:

  1. Verify backward compatibility (test with numeric values if previously supported)
  2. Add documentation (godoc comments)
  3. Consider adding null/empty string handling

Optional Improvements:

  • Add integration test for full config unmarshaling
  • Enhance test coverage for edge cases

Recommendation: Request minor changes to address backward compatibility verification and add documentation before merging.

@claude
Copy link
Contributor

claude bot commented Nov 20, 2025

Code Review - PR #1547: fix(conf): parse IdleTimeout parameter from string

Summary

This PR introduces a custom Duration type wrapper to enable unmarshaling JSON string values (e.g., "5m", "30s") into time.Duration fields, addressing the issue where configuration parameters like IdleTimeout and Timeout need to be parsed from human-readable duration strings.

✅ Strengths

  1. Excellent Solution to the Problem: The custom Duration wrapper is a clean, idiomatic Go approach to handling JSON duration strings. This is the standard pattern for this use case.

  2. Comprehensive Test Coverage: The new confpar_test.go file provides thorough testing for both marshaling and unmarshaling operations, including edge cases like zero duration and invalid input. This is particularly impressive given that the codebase currently has minimal test coverage (only fs/utils/env_test.go).

  3. Backward Compatibility: The changes maintain backward compatibility by:

    • Using the same field names in the JSON schema
    • Supporting the duration string format already documented in the schema
    • Wrapping time.Duration rather than replacing it
  4. Proper Error Handling: The UnmarshalJSON method correctly propagates parsing errors, ensuring invalid duration strings will fail appropriately.

  5. Clean Code Style: The implementation follows Go conventions and matches the project's coding standards as defined in .golangci.yml.

🔍 Observations & Suggestions

1. Consistency Issue: Incomplete Migration

Severity: Medium

The PR updates IdleTimeout to use the new Duration type but leaves AccessesWebhook.Timeout as time.Duration:

// Line 22 - Still using time.Duration
type AccessesWebhook struct {
    Timeout time.Duration     `json:"timeout"` 
}

// Line 68 - Now using Duration wrapper
IdleTimeout Duration `json:"idle_timeout"`

Impact: The AccessesWebhook.Timeout field will still fail to parse from JSON string format (e.g., "timeout": "30s" in config), which is inconsistent with the schema definition and this PR's stated goal.

Recommendation: Update AccessesWebhook.Timeout to also use the Duration type for consistency. The schema already describes it as accepting duration strings.

2. Missing Test Case

Severity: Low

The test suite doesn't verify that the Duration type works correctly when embedded in actual config structs during full JSON unmarshaling.

Recommendation: Consider adding an integration test like:

func TestContentUnmarshalWithDuration(t *testing.T) {
    json := `{"idle_timeout": "5m"}`
    var content Content
    err := json.Unmarshal([]byte(json), &content)
    // verify content.IdleTimeout.Duration == 5 * time.Minute
}

3. Code Location in server/server.go

Severity: Low

The change in server/server.go:204 correctly accesses .Duration to get the underlying time.Duration:

ctx, cancel := context.WithTimeout(context.Background(), s.config.Content.AccessesWebhook.Timeout.Duration)

However, note that AccessesWebhook.Timeout is still time.Duration in the current diff, so this line would need to change if that field is also updated to the Duration wrapper type (see issue #1 above).

4. Marshaling Use Case

Severity: Info

The MarshalJSON method is well-implemented, but it's worth noting when this would be used. Since this is a server configuration that's typically read-only, marshaling may primarily be useful for:

  • Configuration export/backup features
  • Logging/debugging config values
  • Dynamic config generation

This is fine, just noting that the unmarshal path is the critical one for this use case.

🔒 Security Considerations

No security concerns identified. The implementation:

  • Properly validates input via time.ParseDuration
  • Doesn't introduce any injection vulnerabilities
  • Handles errors appropriately without leaking sensitive information

⚡ Performance Considerations

Negligible performance impact. The custom JSON unmarshaling adds minimal overhead (single string parse operation), which only occurs during config file loading at startup.

📊 Test Coverage Assessment

Rating: Good for this PR, but room for improvement overall

  • ✅ The PR adds 54 lines of test code for 21 lines of production code
  • ✅ Tests cover happy path, edge cases, and error conditions
  • ✅ This is excellent coverage for the new Duration type itself
  • ⚠️ Integration testing with actual Content struct unmarshaling would strengthen confidence
  • ℹ️ The overall project test coverage remains low (noted in CLAUDE.md)

🎯 Recommendations Summary

  1. Action Required: Update AccessesWebhook.Timeout to use the Duration type for consistency
  2. Nice to Have: Add integration test for full struct unmarshaling
  3. Documentation: Consider adding a comment explaining why the custom Duration type exists (for future maintainers)

Verdict

This is a well-executed PR that solves the stated problem effectively. The custom Duration type is the correct approach, the tests are comprehensive, and the code quality is high. The main issue is the incomplete migration of the Timeout field, which should also use the new type to maintain consistency across the codebase.

Once the AccessesWebhook.Timeout field is updated to also use Duration, this PR will be ready to merge.

Great work on adding proper test coverage! 👏


Review generated with Claude Code

@claude
Copy link
Contributor

claude bot commented Dec 3, 2025

Code Review for PR #1547: fix(conf): parse IdleTimeout parameter from string

Summary

This PR addresses the issue where IdleTimeout and AccessesWebhook.Timeout parameters couldn't be parsed from JSON strings (e.g., "5m", "30s") despite the schema documentation stating they should accept Go duration format strings. The solution introduces a custom Duration type that wraps time.Duration with custom JSON marshaling/unmarshaling.


✅ Positive Aspects

  1. Good Problem Analysis: The PR correctly identifies that time.Duration doesn't naturally unmarshal from JSON strings, requiring a custom type.

  2. Clean Implementation: The Duration type wrapper is simple and follows a common Go pattern for custom JSON handling.

  3. Comprehensive Tests: The added test file (confpar_test.go) includes:

    • Both marshaling and unmarshaling tests
    • Multiple duration formats (seconds, minutes, hours)
    • Edge cases (zero duration, invalid input)
    • Error handling validation
  4. Schema Alignment: The change properly aligns the code with the existing schema definition that specifies string format with examples like "5m", "30s", "1h".


🐛 Critical Bug

Missing Update in server/server.go:98

The PR changes IdleTimeout from time.Duration to Duration, but doesn't update the code that accesses it:

// Current code (line 98) - WILL NOT COMPILE after this PR
IdleTimeout: int(conf.IdleTimeout.Seconds()),

// Should be:
IdleTimeout: int(conf.IdleTimeout.Duration.Seconds()),

Since Duration is a struct wrapping time.Duration, you need to access the embedded .Duration field. This will cause a compilation error as Seconds() is a method on time.Duration, not on the Duration wrapper type.

Required Fix: Update server/server.go:98 to use .Duration.Seconds() instead of .Seconds().


💡 Suggestions for Improvement

1. Embed time.Duration Instead of Wrapping It

Consider embedding time.Duration to promote its methods:

type Duration struct {
    time.Duration
}

Wait, you already did this! That's good. However, with an embedded field, conf.IdleTimeout.Seconds() should work. Let me reconsider...

Actually, with the current implementation where time.Duration is embedded anonymously, the methods should be promoted. But to be safe and explicit, I'd recommend verifying that conf.IdleTimeout.Seconds() works correctly. If it doesn't, you'll need to use conf.IdleTimeout.Duration.Seconds().

2. Add Integration Test

Consider adding a test that actually unmarshals a full Content struct with these duration fields to ensure they work in the real configuration context:

func TestContentDurationUnmarshal(t *testing.T) {
    jsonData := `{
        "idle_timeout": "5m",
        "accesses_webhook": {
            "url": "http://example.com",
            "timeout": "30s"
        }
    }`
    var content Content
    err := json.Unmarshal([]byte(jsonData), &content)
    // ... assertions
}

3. Consider Zero Value Handling

The schema shows "default": "0s" for idle_timeout. Ensure that:

  • Empty string input is handled gracefully
  • Zero duration ("0s") unmarshals correctly
  • The zero value of Duration{} behaves as expected

Current test covers "0s", which is good. Consider adding a test for empty string if that's a valid input.

4. Document the Type

Add a code comment explaining why this custom type exists:

// Duration wraps time.Duration to allow unmarshaling from JSON strings
// in Go duration format (e.g., "5m", "30s", "1h") as specified in the
// configuration schema.

Your current comment is already good, but could mention it solves a schema compatibility issue.


🔒 Security Considerations

No security concerns identified. The time.ParseDuration function is standard library and handles invalid input safely by returning an error.


📊 Test Coverage

Current Coverage: Good

  • ✅ Marshal/unmarshal roundtrip
  • ✅ Multiple valid duration formats
  • ✅ Invalid input error handling
  • ✅ Zero duration edge case

Missing Coverage:

  • ⚠️ Integration test with actual Content struct unmarshaling
  • ⚠️ Behavior with accesses_webhook being optional (nil pointer)

📝 Code Quality & Style

The code follows Go best practices and aligns with the repository's golangci-lint configuration. The implementation is clean and idiomatic.


⚡ Performance Considerations

Negligible performance impact. JSON unmarshaling will be slightly slower due to the extra time.ParseDuration call, but this only happens during configuration loading (startup and SIGHUP reload), not during request handling.


🎯 Recommendation

Approve with Required Changes

This is a solid fix for a real issue, but requires the critical bug fix in server/server.go:98 before merging. Once that's addressed, this PR will properly implement the schema-specified duration string parsing.

Action Items:

  1. ✅ Verify/fix server/server.go:98 to use conf.IdleTimeout.Duration.Seconds() or confirm that method promotion works
  2. ✅ Run go build -v ./... to verify compilation
  3. ✅ Run go test -race -v ./... to verify all tests pass
  4. 🔄 Consider adding integration test (optional but recommended)

Great work on identifying and fixing this schema mismatch! 🚀

@asarandi asarandi force-pushed the unmarshal-duration-from-string branch from e42ceda to 1cc3441 Compare December 3, 2025 06:35
@claude
Copy link
Contributor

claude bot commented Dec 3, 2025

Code Review Feedback

Thank you for this PR! This is a well-thought-out fix that properly addresses the mismatch between the JSON schema (which specifies strings like "5m", "30s") and the Go code (which was trying to unmarshal directly into time.Duration).

Strengths

  1. Correct Problem Identification: The PR correctly identifies that time.Duration cannot be directly unmarshaled from JSON strings. The schema at config-schema.json:63-73 clearly specifies string format (e.g., "5m", "30s"), but Go's time.Duration unmarshals from integers (nanoseconds).

  2. Clean Implementation: The custom Duration wrapper type is a standard Go pattern for this exact use case. The implementation is clean and minimal.

  3. Good Test Coverage: The tests cover both marshaling and unmarshaling directions, including:

    • Various duration formats (seconds, minutes, hours)
    • Zero duration edge case
    • Error handling for invalid input
  4. Consistent Application: The fix is applied to both IdleTimeout and AccessesWebhook.Timeout, ensuring consistency across the codebase.

  5. Backward Compatibility: The .Duration field access in server.go:204 ensures existing code continues to work seamlessly.

🔍 Suggestions & Considerations

1. Test Case Enhancement

Consider adding a test for the zero/empty string case since the schema description mentions "Set to 0 or empty to disable idle timeout":

{"0s", 0, false},
{"", 0, true},  // Empty string case

This would verify behavior when users omit the field entirely or provide an empty string.

2. Error Handling in UnmarshalJSON

The current implementation is good, but consider adding a more descriptive error message:

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return fmt.Errorf("duration must be a string: %w", err)
    }
    var err error
    d.Duration, err = time.ParseDuration(s)
    if err != nil {
        return fmt.Errorf("invalid duration format %q: %w", s, err)
    }
    return nil
}

This would help users debug configuration errors more easily.

3. Documentation

The code comment at confpar.go:79-81 is excellent. Consider whether the type should be exported and documented in any user-facing documentation or README files about configuration.

4. Nil Pointer Safety

In server.go:204, consider adding a nil check for AccessesWebhook.Timeout to prevent potential panics:

timeout := 30 * time.Second // default
if s.config.Content.AccessesWebhook != nil && s.config.Content.AccessesWebhook.Timeout.Duration > 0 {
    timeout = s.config.Content.AccessesWebhook.Timeout.Duration
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)

Though this depends on whether AccessesWebhook can be nil (it's a pointer field).

🎯 Code Quality Assessment

  • Follows Repository Standards: Adheres to golangci-lint configuration
  • Proper Package Structure: Test file correctly placed in same package
  • Race Detection Compatible: Tests will run with -race flag per CLAUDE.md
  • No Over-engineering: Minimal, focused solution without unnecessary abstractions
  • Security: No security concerns introduced

📊 Performance

No performance concerns. The JSON marshaling/unmarshaling overhead is negligible and only occurs during configuration loading.

🏁 Recommendation

Approve with minor suggestions. The core implementation is solid and ready to merge. The suggestions above are optional enhancements that could improve error messages and edge case handling, but they don't block merging this PR.

Great work! This fix ensures the code matches the documented schema and will prevent user confusion when configuring timeout values.

@claude
Copy link
Contributor

claude bot commented Dec 3, 2025

Pull Request Review: Fix IdleTimeout Parameter Parsing

Summary

This PR addresses a critical issue where idle_timeout and timeout (in accesses_webhook) were not properly unmarshaling from JSON strings as documented in the schema. The solution implements a custom Duration type wrapper that correctly handles Go duration format strings (e.g., "5m", "30s", "1h").

Strengths

  1. Correct Problem Identification - The PR correctly identifies the mismatch between the JSON schema (which specifies string type) and the Go struct (which used time.Duration directly). idle_timeout is documented in config-schema.json:63-74 as a string type with examples like "5m", "30s", "1h"

  2. Clean Implementation - The custom Duration type wrapper is a standard Go pattern for handling duration strings in JSON. Both MarshalJSON and UnmarshalJSON are implemented for bidirectional conversion. The implementation properly embeds time.Duration, preserving all its methods.

  3. Comprehensive Test Coverage - Excellent addition of confpar_test.go (this package previously had no tests). Tests cover both marshaling and unmarshaling with good edge case coverage: zero duration, various time units. Error handling is tested with invalid input.

  4. Minimal Breaking Changes - Uses embedded field pattern, so existing code accessing .Duration continues to work. Only requires adding .Duration suffix in server/server.go:204 for context timeout.

Potential Issues and Suggestions

  1. Missing Schema Update for accesses_webhook.timeout - The PR updates the AccessesWebhook.Timeout field but there is no corresponding entry in config-schema.json. Consider adding schema documentation for the webhook timeout field.

  2. Test Coverage Gaps - Consider adding tests for: negative durations, very large durations, empty string handling, and actual config unmarshaling in an integration test.

  3. Documentation - The godoc comment for Duration is good, but could mention the JSON format more explicitly.

Security Considerations

No security issues identified: time.ParseDuration handles malformed input safely, no potential for injection attacks, no sensitive data exposure.

Performance Considerations

Performance impact is negligible: unmarshaling happens only during config load/reload (infrequent operation), time.ParseDuration is efficient and well-tested.

Code Quality

Meets repository standards: follows Go idioms and patterns, consistent with existing codebase style, function length within .golangci.yml limits, proper error handling.

Final Verdict

Recommendation: Approve with minor suggestions

This is a solid fix that properly addresses the issue. The implementation is clean, well-tested, and follows Go best practices. The only missing piece is the JSON schema documentation for accesses_webhook.timeout, but this does not block the functionality.

Testing Checklist:

  • Unit tests added and comprehensive
  • Changes are backward compatible (with .Duration access)
  • Integration test with actual config file (nice to have)
  • Schema documentation complete (minor gap)

Great work on identifying and fixing this issue!

@claude
Copy link
Contributor

claude bot commented Dec 3, 2025

Code Review for PR #1547

This PR introduces a custom Duration type to properly parse IdleTimeout and Timeout fields from JSON strings (e.g., "5m", "30s") instead of integer nanoseconds. This aligns with the config-schema.json specification.

Positive Aspects

  • Correct solution using standard Go JSON unmarshaling patterns
  • Comprehensive unit tests added (confpar_test.go)
  • Minimal, surgical code changes
  • Fixes both IdleTimeout and AccessesWebhook.Timeout consistently

Issues & Recommendations

1. Breaking Change (Medium Severity)
Existing configs with integer values will fail to parse. Consider:

  • Documenting the breaking change
  • Adding backward compatibility for integer parsing
  • Providing migration guidance

2. Test Coverage (Low Severity)

  • Add integration test for unmarshaling complete Content/AccessesWebhook structs
  • Add test case for empty string handling

3. Code Quality (Low Severity)

  • Consider wrapping ParseDuration errors with better context
  • The comment at confpar.go:80-81 could mention why this type exists (schema alignment)

Security & Performance

✅ No security concerns - uses stdlib functions safely
✅ No performance issues - parsing at config load only

Recommendation

Approve with minor suggestions. Well-implemented fix following Go best practices. Main concern is the breaking change which should be documented or mitigated.

Before merging, verify:

  • go test -race -v ./... passes
  • golangci-lint run passes
  • Manual test with string-based config
  • Migration path for existing deployments

@claude
Copy link
Contributor

claude bot commented Dec 3, 2025

Code Review for PR #1547: Fix IdleTimeout parameter parsing

Summary

This PR successfully addresses the mismatch between the JSON schema (which expects a string in Go duration format like "5m", "30s") and the Go code (which was using time.Duration directly, expecting numeric nanoseconds). The solution introduces a custom Duration wrapper type with proper JSON marshaling/unmarshaling.


✅ Strengths

  1. Correct Problem Identification: The PR correctly identifies that idle_timeout is defined as a string type in config-schema.json:63-73 but was being parsed as a numeric time.Duration.

  2. Clean Implementation: The custom Duration type is well-designed with proper JSON marshaling.

  3. Good Test Coverage: The new confpar_test.go includes comprehensive table-driven tests.

  4. Consistency: Applied the fix to both IdleTimeout and AccessesWebhook.Timeout fields.


🔍 Issues & Suggestions

1. Potential Zero-Value Behavior Issue

The UnmarshalJSON only handles the string case. If JSON contains null or the field is omitted, this will fail with a JSON unmarshal error instead of defaulting to 0s.

Suggested Fix:

func (d *Duration) UnmarshalJSON(b []byte) (err error) {
    if string(b) == "null" {
        d.Duration = 0
        return nil
    }
    var s string
    if err = json.Unmarshal(b, &s); err == nil {
        if s == "" {
            d.Duration = 0
            return nil
        }
        d.Duration, err = time.ParseDuration(s)
    }
    return
}

2. Test Coverage Gaps

Missing test cases for:

  • Empty string
  • Null value
  • Omitted field (integration test with full struct)
  • Complex durations like "1h30m45s"

3. Documentation Enhancement

Consider expanding the comment to explain why this type exists (schema compatibility).


🔒 Security Considerations

✅ No security issues - uses safe standard library functions.


🚀 Performance Considerations

✅ Minimal impact - config parsing is infrequent.


✨ Final Verdict

Recommendation: ✅ Approve with minor suggestions

Main action items:

  1. Add null/empty string handling in UnmarshalJSON
  2. Expand test coverage for edge cases

Estimated risk: Low - backward compatible and fixes the schema mismatch.

@claude
Copy link
Contributor

claude bot commented Dec 4, 2025

Code Review: PR #1547 - Parse IdleTimeout parameter from string

Overview

This PR fixes the configuration parsing for duration fields to match the JSON schema specification, which expects string values in Go duration format (e.g., "5m", "30s", "1h") rather than numeric values.


Strengths

1. Correct Problem Identification

The PR correctly addresses a mismatch between the JSON schema (config-schema.json:63-74) which defines idle_timeout as type string, and the Go struct which used time.Duration (a numeric type). This made it impossible to parse config files that followed the documented schema.

2. Well-Designed Solution

The custom Duration wrapper type is a clean, idiomatic Go solution:

  • Embeds time.Duration for seamless use throughout the codebase
  • Implements json.Marshaler and json.Unmarshaler interfaces
  • Allows natural usage as duration.Duration when the underlying value is needed

3. Comprehensive Test Coverage

The new test file config/confpar/confpar_test.go provides excellent coverage:

  • Tests both marshaling and unmarshaling
  • Covers multiple duration formats (seconds, minutes, hours)
  • Tests edge cases (0s, invalid input)
  • Validates error handling

4. Minimal Code Changes

The PR makes surgical changes only where needed:

  • confpar.go: Type changes and new wrapper (27 lines)
  • server.go:204: Single field access update (.Duration suffix)
  • Zero breaking changes to the public API

🔍 Potential Issues & Suggestions

1. Missing Webhook Timeout Update

The AccessesWebhook.Timeout field (confpar.go:22) should also be updated to use the new Duration type for consistency:

type AccessesWebhook struct {
    URL     string            `json:"url"`
    Headers map[string]string `json:"headers"`
    Timeout Duration          `json:"timeout"`  // Should use Duration wrapper
}

Without this change, webhook timeout configuration likely has the same parsing issue. The fix in server.go:204 suggests this field is also used, but it wasn't updated to use the wrapper type.

2. Incomplete Error Context

In UnmarshalJSON, consider adding more context to parsing errors:

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return fmt.Errorf("duration must be a string: %w", err)
    }
    var err error
    d.Duration, err = time.ParseDuration(s)
    if err != nil {
        return fmt.Errorf("invalid duration format %q: %w", s, err)
    }
    return nil
}

This would help users debug configuration errors more easily.

3. Test Coverage - Empty String

Consider adding a test case for empty string ("") since the schema description mentions "Set to 0 or empty to disable idle timeout":

{`"", 0, false},  // Empty string should be valid

4. Backward Compatibility Documentation

This is a breaking change for existing config files that may have used numeric values (even though they violated the schema). Consider:

  • Adding a comment in the code explaining the change
  • Documenting migration steps in release notes
  • Potentially adding a deprecation warning if numeric values are detected

🔒 Security Considerations

No security concerns identified. The change:

  • Properly validates input via time.ParseDuration
  • Doesn't introduce any injection vulnerabilities
  • Maintains existing timeout protection mechanisms

Performance Considerations

  • Minimal performance impact: JSON unmarshaling happens once at startup
  • No runtime overhead after configuration is loaded
  • The embedded time.Duration ensures zero-cost abstraction

📋 Code Quality & Style

  • ✅ Follows Go naming conventions
  • ✅ Includes helpful comments explaining the wrapper's purpose
  • ✅ Test structure follows Go testing best practices
  • ✅ Consistent with repository's code style (per golangci-lint configuration)

🎯 Recommendations

  1. High Priority: Update AccessesWebhook.Timeout to also use the Duration wrapper
  2. Medium Priority: Enhance error messages with more context
  3. Low Priority: Add test case for empty string
  4. Documentation: Note the breaking change in commit message/release notes

Verdict

This is a well-implemented fix for a legitimate bug. The code quality is high, and tests are comprehensive. With the addition of the webhook timeout fix, this would be ready to merge.

Recommendation: Request changes to also fix AccessesWebhook.Timeout for consistency, then approve.

@claude
Copy link
Contributor

claude bot commented Dec 5, 2025

Code Review - PR #1547: Parse IdleTimeout parameter from string

Thank you for this fix! This PR correctly addresses the issue where the IdleTimeout parameter wasn't being parsed from JSON strings as documented in the schema. Here's my detailed review:


Strengths

  1. Correct Problem Resolution: The custom Duration type wrapper properly handles JSON unmarshaling from string format (e.g., "5m", "30s") which matches the JSON schema specification at line 63-74 of config-schema.json.

  2. Comprehensive Test Coverage: The new test file config/confpar/confpar_test.go is excellent:

    • Tests both marshaling and unmarshaling
    • Includes valid inputs (5m, 30s, 1h, 0s)
    • Tests error handling for invalid input
    • Well-structured with clear test cases
  3. Minimal Changes: The fix is surgical - only changes what's needed:

    • Type changes in 2 locations (IdleTimeout and Timeout in AccessesWebhook)
    • One .Duration accessor added in server/server.go:204
    • Clean implementation of the wrapper type
  4. Consistent Application: Both IdleTimeout and AccessesWebhook.Timeout are updated to use the new Duration type, ensuring consistency across the configuration.

  5. Good Documentation: The comment on the Duration type clearly explains its purpose.


🔍 Observations & Suggestions

1. Missing JSON Schema Update for accesses_webhook.timeout

The AccessesWebhook.Timeout field was also changed to use the custom Duration type, but there's no corresponding entry in config-schema.json. Consider adding:

"accesses_webhook": {
  "type": "object",
  "properties": {
    "url": { "type": "string" },
    "headers": { "type": "object" },
    "timeout": {
      "type": "string",
      "description": "Maximum time the webhook request can take. Use Go duration format (e.g., '5s', '30s', '1m').",
      "examples": ["30s", "5s", "1m"]
    }
  }
}

2. Test Case Enhancement

Consider adding a test case for unmarshaling into a struct to verify the integration works end-to-end:

func TestDurationInStruct(t *testing.T) {
    type Config struct {
        Timeout Duration `json:"timeout"`
    }
    
    var cfg Config
    err := json.Unmarshal([]byte(`{"timeout":"5m"}`), &cfg)
    if err != nil {
        t.Fatalf("json.Unmarshal(): %v", err)
    }
    if cfg.Timeout.Duration != 5*time.Minute {
        t.Fatalf("have:%v want:%v", cfg.Timeout.Duration, 5*time.Minute)
    }
}

3. Zero Value Handling

The current implementation handles "0s" correctly, but what about empty strings or null values? The schema defaults to "0s". Consider explicitly testing:

  • Empty string: ""
  • JSON null: null

This ensures backward compatibility with configs that might not have these fields set.

4. Error Message Quality

The UnmarshalJSON method returns the raw error from time.ParseDuration. Consider wrapping it for better debugging:

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    var err error
    d.Duration, err = time.ParseDuration(s)
    if err != nil {
        return fmt.Errorf("invalid duration format %q: %w", s, err)
    }
    return nil
}

🔒 Security Considerations

✅ No security concerns identified. The time.ParseDuration function is safe and doesn't introduce vulnerabilities.


Performance Considerations

✅ Negligible performance impact. The duration parsing happens only during config load, which is infrequent.


📋 Summary

This is a well-implemented fix that correctly addresses issue #1519. The code is clean, tested, and follows Go best practices. The suggestions above are minor enhancements that would make the implementation even more robust.

Recommendation: ✅ Approve with minor suggestions

The PR is ready to merge as-is, but consider the JSON schema update for completeness.

@claude
Copy link
Contributor

claude bot commented Dec 5, 2025

Code Review

Thank you for this contribution! This PR addresses an important issue with configuration parsing for duration fields. Here's my detailed feedback:

✅ Positive Aspects

  1. Correct Problem Identification: The PR correctly addresses the mismatch between the JSON schema (which specifies type: string) and the Go code (which used time.Duration directly). The standard library's time.Duration unmarshals from JSON as an integer (nanoseconds), not as a human-readable string like "5m".

  2. Good Test Coverage: The new test file confpar_test.go includes comprehensive tests for both marshaling and unmarshaling, covering:

    • Valid duration strings ("5m", "30s", "1h", "0s")
    • Invalid input handling
    • Edge cases (zero duration)
  3. Consistent Implementation: The custom Duration type properly embeds time.Duration and implements both MarshalJSON and UnmarshalJSON methods symmetrically.

  4. Proper Usage Updates: The PR correctly updates the usage in server/server.go:204 to access .Duration from the wrapper type.

🔍 Issues & Concerns

1. Potential Breaking Change for Numeric Values (Medium Severity)

The new implementation only accepts string values. If any existing configuration files use numeric values (which time.Duration accepts natively), this will break backward compatibility.

Example of potentially breaking config:

{
  "idle_timeout": 300000000000
}

Recommendation: Consider supporting both formats during unmarshaling:

func (d *Duration) UnmarshalJSON(b []byte) (err error) {
    // Try string format first (preferred)
    var s string
    if err = json.Unmarshal(b, &s); err == nil {
        d.Duration, err = time.ParseDuration(s)
        return
    }
    
    // Fall back to numeric format for backward compatibility
    var ns int64
    if err = json.Unmarshal(b, &ns); err == nil {
        d.Duration = time.Duration(ns)
        return nil
    }
    
    return err
}

2. Inconsistency with AccessesWebhook.Timeout (Low Severity)

The PR updates IdleTimeout but leaves AccessesWebhook.Timeout as time.Duration (line 22). For consistency, this should also use the new Duration type if the schema expects a string format.

Files to check:

  • config/confpar/confpar.go:22 - AccessesWebhook.Timeout
  • Verify the schema definition for this field

3. Missing Schema Documentation (Low Severity)

The accesses_webhook field doesn't appear in config-schema.json, which may indicate incomplete schema documentation. Consider adding it to maintain schema completeness.

4. Test Edge Case Missing (Minor)

The tests don't cover the case where an empty string is provided:

{"invalid", 0, true},  // covered
{"", ???, ???},        // not covered

📝 Code Quality Notes

  1. Line 98 in server/server.go: The conversion to seconds works correctly with the embedded Duration field: conf.IdleTimeout.Duration.Seconds() would be more explicit, but the current conf.IdleTimeout.Seconds() works due to embedding.

  2. Test Structure: The table-driven tests follow Go best practices and are well-organized.

  3. Naming: The Duration type name is clear and follows Go conventions.

🔒 Security Considerations

No security concerns identified. The time.ParseDuration function is safe and doesn't introduce vulnerabilities.

⚡ Performance Considerations

Minimal performance impact. The custom unmarshaling adds negligible overhead compared to direct time.Duration unmarshaling.

🎯 Recommendations

Before merging:

  1. HIGH: Add backward compatibility for numeric duration values (see suggestion above)
  2. MEDIUM: Update AccessesWebhook.Timeout to use the same Duration type for consistency
  3. LOW: Add test case for empty string input
  4. LOW: Consider updating/adding schema documentation for webhook timeout field

Testing:

  • Run go test -race -v ./... to verify all tests pass
  • Run golangci-lint run to ensure code quality standards
  • Test with an actual configuration file using string format ("5m")

Summary

This is a solid fix that addresses a real schema-to-code mismatch. With the backward compatibility enhancement and consistency improvements, this will be a great addition to the codebase. The test coverage is commendable!

Overall Assessment: Approve with suggestions for improvements before merge.

@claude
Copy link
Contributor

claude bot commented Dec 8, 2025

Code Review

Thank you for this PR! This is a well-crafted fix that addresses a real issue with JSON parsing of duration fields. Here's my detailed review:


Strengths

  1. Solves a Real Problem: The schema (config-schema.json:63-74) documents idle_timeout as a string with Go duration format ("5m", "30s"), but the code was using time.Duration which unmarshals from JSON integers (nanoseconds). This mismatch would cause config parsing failures.

  2. Clean Implementation: The custom Duration wrapper type is a standard Go pattern for custom JSON marshaling/unmarshaling. The implementation is clean and follows Go conventions.

  3. Comprehensive Tests: The test coverage is excellent with:

    • Multiple valid duration formats tested
    • Error case handling for invalid input
    • Both marshal and unmarshal operations tested
  4. Consistent Application: The fix is applied to both IdleTimeout and AccessesWebhook.Timeout, ensuring consistency across the codebase.

  5. Proper Usage Update: server/server.go:204 correctly accesses .Duration field from the wrapper type.


🔍 Code Quality Observations

Good:

  • Follows Go naming conventions
  • Proper error handling in UnmarshalJSON
  • Embedded time.Duration allows direct access to all Duration methods
  • Line length within 120 character limit per .golangci.yml

Minor suggestion (optional):
The MarshalJSON method could potentially handle edge cases more explicitly, though the current implementation using String() is the idiomatic approach and should work correctly.


🐛 Potential Issues

None identified. The code correctly:

  • Handles the conversion in both directions
  • Returns errors appropriately
  • Maintains backward compatibility (duration strings were already in the schema)

Performance Considerations

Minimal impact:

  • The custom marshaling/unmarshaling adds negligible overhead
  • Only executed during config loading (not hot path)
  • time.ParseDuration is already optimized in the standard library

🔒 Security Concerns

None identified:

  • No user input validation issues (ParseDuration handles malformed input safely)
  • No injection vulnerabilities
  • Proper error propagation

🧪 Test Coverage

Excellent:

  • New test file config/confpar/confpar_test.go provides comprehensive coverage
  • Tests both marshal and unmarshal paths
  • Includes error cases
  • Uses table-driven tests (Go best practice)

Recommendation: Consider adding an integration test that:

  1. Creates a sample JSON config with idle_timeout: "5m"
  2. Unmarshals it into Content struct
  3. Verifies the duration is correctly parsed

This would validate the entire parsing flow, though it's not critical given the unit test coverage.


📋 Checklist


💡 Recommendation

LGTM - Approve and merge pending:

  1. CI tests passing (golangci-lint and go test -race)
  2. Confirming this fixes the original issue report

This is a well-implemented fix that improves the codebase by aligning the implementation with the documented schema.

@claude
Copy link
Contributor

claude bot commented Dec 9, 2025

Code Review

Thank you for this PR! This is a well-implemented fix for JSON parsing of duration fields. Here's my detailed review:

Strengths

  1. Correct Problem Identification: The PR correctly addresses the mismatch between the JSON schema (expecting string durations like "5m") and the Go code (using time.Duration which unmarshals from nanoseconds).

  2. Clean Implementation: The custom Duration type with UnmarshalJSON/MarshalJSON methods is the standard Go pattern for this problem. The embedded time.Duration provides direct access to all duration methods.

  3. Excellent Test Coverage: The new test file confpar_test.go is comprehensive:

    • Tests both marshal and unmarshal paths
    • Includes edge cases (0s, invalid input)
    • Covers multiple duration formats (seconds, minutes, hours)
    • Validates error handling for invalid input
  4. Minimal Changes: The PR only touches what's necessary - two duration fields and one usage site. This reduces risk.

  5. Consistent Application: Both IdleTimeout and AccessesWebhook.Timeout are updated, ensuring consistency across the codebase.


🔍 Potential Issues & Suggestions

1. Breaking Change for Existing Configs

Severity: Medium

The current code accepts numeric values (nanoseconds) in JSON. After this change, only string durations are accepted. This could break existing configurations that use numeric values.

Example of what will break:

{
  "idle_timeout": 300000000000  // 5 minutes in nanoseconds - will fail after this PR
}

Recommendation: Consider supporting both formats for backward compatibility:

func (d *Duration) UnmarshalJSON(b []byte) error {
    // Try parsing as string first (new format)
    var s string
    if err := json.Unmarshal(b, &s); err == nil {
        var parseErr error
        d.Duration, parseErr = time.ParseDuration(s)
        return parseErr
    }
    
    // Fall back to numeric format (old format, nanoseconds)
    var ns int64
    if err := json.Unmarshal(b, &ns); err == nil {
        d.Duration = time.Duration(ns)
        return nil
    }
    
    return fmt.Errorf("invalid duration format")
}

Add a test case:

{300000000000, 5 * time.Minute, false}, // numeric nanoseconds (legacy format)

2. Missing Zero Value Test Case

Severity: Low

The unmarshal tests include "0s" but could also test empty string behavior since the schema allows it:

{"", 0, false}, // empty string should be valid per schema

3. Server Usage Site Needs Adjustment

Severity: Low

In server/server.go:98, the code accesses .Duration explicitly:

IdleTimeout: int(conf.IdleTimeout.Duration.Seconds()),

While this works due to embedding, it's slightly verbose. Since Duration embeds time.Duration, you could simplify to:

IdleTimeout: int(conf.IdleTimeout.Seconds()),

However, the current approach is more explicit, so this is optional.


4. Documentation/Migration Notes

Severity: Low

Consider adding a comment in the Duration type explaining the rationale and migration path:

// Duration wraps time.Duration to allow unmarshaling from JSON strings
// in Go duration format (e.g., "5m", "30s", "1h").
// 
// This is necessary because time.Duration unmarshals from nanoseconds (int64)
// by default, but our JSON schema expects human-readable string formats.
//
// For backward compatibility, numeric values (nanoseconds) are also supported.
type Duration struct {
    time.Duration
}

🔒 Security Considerations

✅ No security concerns identified. Duration parsing is bounded by Go's time.ParseDuration which has safe limits.


Performance Considerations

✅ Performance impact is negligible:

  • Parsing happens only at config load time (infrequent operation)
  • time.ParseDuration is efficient
  • No additional allocations in hot paths

📋 Test Coverage

Strong test coverage for the new code:

  • Tests both serialization directions
  • Validates error cases
  • Uses table-driven tests (Go best practice)

Suggestion: Add an integration test that loads a sample config file with these duration fields to ensure end-to-end functionality.


🎯 Code Quality & Best Practices

✅ Follows Go conventions:

  • Proper error handling
  • Idiomatic JSON marshaling pattern
  • Table-driven tests
  • Clear, descriptive comments

✅ Aligns with .golangci.yml standards:

  • Proper formatting
  • No linting violations expected

📝 Final Recommendation

Approve with minor suggestions

The PR is well-implemented and solves a real problem. The main consideration is whether to support backward compatibility with numeric duration values. If you're confident no existing configs use numeric values (or a breaking change is acceptable), this can merge as-is.

Otherwise, I recommend adding backward compatibility support as outlined above to ensure smooth upgrades for existing users.

Great work on the comprehensive test coverage! 🎉

@claude
Copy link
Contributor

claude bot commented Dec 11, 2025

Code Review: PR #1547 - Parse IdleTimeout parameter from string

Summary

This PR addresses a type mismatch between the JSON schema definition (string) and the Go struct field (time.Duration) for the idle_timeout and timeout (in AccessesWebhook) configuration parameters. The solution introduces a custom Duration type wrapper that properly unmarshals Go duration strings (e.g., "5m", "30s", "1h") from JSON.


✅ Positives

  1. Correct Problem Identification: The PR fixes a real issue where the JSON schema defines idle_timeout as a string with examples like "5m", "30s", but the Go code expected a numeric duration value.

  2. Clean Implementation: The Duration wrapper type is well-designed with proper JSON marshaling/unmarshaling methods that maintain backward compatibility.

  3. Good Test Coverage: The new confpar_test.go includes both marshaling and unmarshaling tests with multiple test cases, including error handling.

  4. Consistent Application: The fix is applied to both IdleTimeout in Content and Timeout in AccessesWebhook, ensuring consistency across similar fields.


🔍 Issues & Suggestions

1. Missing .Duration Access in server.go ⚠️ CRITICAL

// Line 204 in server/server.go (from diff)
ctx, cancel := context.WithTimeout(context.Background(), s.config.Content.AccessesWebhook.Timeout.Duration)

Issue: This correctly accesses .Duration to unwrap the time.Duration value.

However, line 98 in server.go still uses:

IdleTimeout: int(conf.IdleTimeout.Seconds()),

This should be:

IdleTimeout: int(conf.IdleTimeout.Duration.Seconds()),

Without this fix, the code will not compile or will produce incorrect results. Please verify this is updated in the PR - the diff doesn't show changes to line 98, suggesting this may need attention.

2. Exported Duration Type Without Documentation

The Duration type and its methods are exported but lack package-level documentation:

// Duration wraps time.Duration to allow unmarshaling from JSON strings
// in Go duration format (e.g., "5m", "30s", "1h")
type Duration struct {
    time.Duration
}

Recommendation: Add godoc comments to the MarshalJSON and UnmarshalJSON methods as well.

3. Test Coverage Could Be Enhanced

Current tests are good but could include:

  • Testing zero/empty string input: "" (what should happen?)
  • Testing negative durations: "-5m"
  • Testing very large durations
  • Testing malformed JSON (non-string values)

Example additional test case:

{input: `""`, want: 0, wantErr: false}, // or wantErr: true depending on desired behavior
{input: `-5m`, want: -5 * time.Minute, wantErr: false},

4. Error Handling Could Be More Informative

func (d *Duration) UnmarshalJSON(b []byte) (err error) {
    var s string
    if err = json.Unmarshal(b, &s); err == nil {
        d.Duration, err = time.ParseDuration(s)
    }
    return
}

Suggestion: Consider wrapping errors with context:

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    duration, err := time.ParseDuration(s)
    if err != nil {
        return fmt.Errorf("invalid duration format %q: %w", s, err)
    }
    d.Duration = duration
    return nil
}

This would make debugging configuration errors much easier for users.


🔒 Security Considerations

✅ No security concerns identified. The code properly validates input through Go's standard time.ParseDuration, which is safe.


⚡ Performance Considerations

✅ No performance concerns. The duration parsing happens only during configuration loading, not in the hot path.


🧪 Testing Recommendations

  1. Run the tests: go test -v ./config/confpar/
  2. Integration test: Create a sample config file with "idle_timeout": "5m" and verify the server loads it correctly
  3. Verify backward compatibility: If there are existing configs with numeric values, ensure they still work (though unlikely given the schema)

📋 Checklist Before Merge

  • Verify line 98 in server/server.go correctly accesses .Duration
  • Run go test -race -v ./... to ensure all tests pass
  • Run golangci-lint run to check for linting issues
  • Consider adding error context to UnmarshalJSON
  • Consider expanding test coverage

Overall Assessment

Recommendation: ✅ Approve with minor fixes

This is a well-executed fix for a legitimate configuration parsing issue. The custom Duration type is the right solution, and the tests demonstrate the expected behavior. The main concern is ensuring all code locations properly access .Duration when using the wrapped type.

Great work on identifying and fixing this schema/code mismatch! 🎉

@claude
Copy link
Contributor

claude bot commented Dec 18, 2025

Pull Request Review

Thank you for this fix! The changes correctly address the mismatch between the JSON schema (which expects duration strings) and the Go code (which was trying to unmarshal directly into time.Duration).

✅ Positives

  1. Correct Problem Identification: The PR correctly identifies that idle_timeout and webhook timeout fields are documented as strings (e.g., "5m", "30s") in the schema but were being unmarshaled as numeric time.Duration values, which would fail.

  2. Clean Solution: The custom Duration wrapper type with UnmarshalJSON/MarshalJSON methods is the idiomatic Go approach for this problem.

  3. Good Test Coverage: The new test file confpar_test.go covers:

    • Marshaling various durations
    • Unmarshaling valid duration strings
    • Error handling for invalid input
    • Edge cases (0s duration)
  4. Minimal Changes: The fix touches only what's necessary, changing time.Duration to Duration in two places and adding the wrapper type.

  5. Backward Compatible: The change maintains the ability to work with duration values programmatically while fixing JSON parsing.

🔍 Observations & Suggestions

1. Missing .Duration accessor in server.go:98 ✅ Already Fixed

The PR correctly updates server/server.go:204 to use .Duration when accessing the webhook timeout:

ctx, cancel := context.WithTimeout(context.Background(), s.config.Content.AccessesWebhook.Timeout.Duration)

However, I notice server/server.go:98 also uses IdleTimeout:

IdleTimeout: int(conf.IdleTimeout.Seconds()),

This needs to be updated to:

IdleTimeout: int(conf.IdleTimeout.Duration.Seconds()),

Update: Looking at the diff again, this appears to be handled by the type change. Since Duration embeds time.Duration, the .Seconds() method should be promoted. However, it would be clearer and more consistent to explicitly use .Duration.Seconds().

2. Test Coverage - Edge Cases

Consider adding tests for:

  • Very large durations (e.g., "24h", "168h")
  • Negative durations (if they should be rejected)
  • Empty string ("" - currently would fail, might want explicit handling)

3. Error Messages

The UnmarshalJSON returns the raw time.ParseDuration error. Consider wrapping it for better context:

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return fmt.Errorf("expected duration string: %w", err)
    }
    var err error
    d.Duration, err = time.ParseDuration(s)
    if err != nil {
        return fmt.Errorf("invalid duration format: %w", err)
    }
    return nil
}

4. Documentation

The godoc comment on Duration is good, but consider noting that this type is specifically for JSON compatibility with the config schema.

5. Type Embedding Consideration

The embedded time.Duration means all time.Duration methods are promoted. This is good for usability but means the type can be used directly without .Duration. This is actually a pro, not a con - nice design choice!

🔒 Security

No security concerns identified. The parsing relies on Go's standard time.ParseDuration which is safe.

⚡ Performance

Negligible impact. The unmarshaling happens once at config load time. No runtime performance concerns.

🏗️ Code Quality

The code follows the repository's standards:

  • Proper package documentation
  • Clean, readable implementation
  • Follows Go conventions
  • Consistent with existing patterns in the codebase

📋 Suggested Action Items

  1. Critical: Verify that server/server.go:98 works correctly with the embedded Duration. If there are any compilation issues, use .Duration.Seconds() explicitly.
  2. Optional: Add error message wrapping for better debugging
  3. Optional: Expand test coverage for edge cases
  4. Nice to have: Update config-schema.json to document the webhook timeout field (appears to be missing from the schema)

✅ Recommendation

Approve with minor suggestions. The core fix is solid and solves the problem correctly. The test coverage is good. The suggestions above are nice-to-haves, not blockers.

Great work! 🎉

@claude
Copy link
Contributor

claude bot commented Dec 18, 2025

Code Review

Thank you for this PR! This is a well-thought-out fix that addresses the mismatch between the JSON schema specification (which expects duration strings like "5m") and the Go code (which was trying to unmarshal directly into time.Duration).

✅ Strengths

  1. Solves the Core Problem: The custom Duration type wrapper correctly allows unmarshaling from JSON strings in Go duration format (e.g., "5m", "30s"), which matches the schema definition in config-schema.json:62-73.

  2. Good Test Coverage: The new test file confpar_test.go includes:

    • Marshaling tests with various durations (0s, 31s, 5m, 1h)
    • Unmarshaling tests with valid inputs
    • Error handling test for invalid input
  3. Consistent Application: Both IdleTimeout and AccessesWebhook.Timeout fields are updated, ensuring consistency across the codebase.

  4. Backwards Compatible: The .Duration field exposure means existing code like server/server.go:98 continues to work with minimal changes (just adding .Duration).

🔍 Potential Issues & Suggestions

1. Missing Numeric Fallback (Medium Priority)

The current implementation only handles string inputs. However, the original time.Duration type in Go JSON unmarshaling accepts numeric values as nanoseconds. Users might have existing configs with numeric values:

{
  "idle_timeout": 300000000000  // 5 minutes in nanoseconds
}

Recommendation: Add support for numeric inputs as a fallback:

func (d *Duration) UnmarshalJSON(b []byte) error {
	// Try string first (preferred format)
	var s string
	if err := json.Unmarshal(b, &s); err == nil {
		d.Duration, err = time.ParseDuration(s)
		return err
	}
	
	// Fallback to numeric (nanoseconds) for backwards compatibility
	var ns int64
	if err := json.Unmarshal(b, &ns); err == nil {
		d.Duration = time.Duration(ns)
		return nil
	}
	
	return fmt.Errorf("invalid duration format")
}

2. Zero Value Handling (Low Priority)

Empty string ("") currently fails to parse. Based on the schema description ("Set to 0 or empty to disable idle timeout"), empty strings should probably be treated as 0:

func (d *Duration) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err == nil {
		if s == "" {
			d.Duration = 0
			return nil
		}
		d.Duration, err = time.ParseDuration(s)
		return err
	}
	// ... numeric fallback
}

Add a test case:

{`""`, 0, false},  // empty string should be treated as 0

3. Test Coverage Gaps (Low Priority)

Consider adding tests for:

  • Large duration values (e.g., "24h", "168h")
  • Negative durations (should they be rejected?)
  • Edge cases like "0" (no unit)
  • Multiple unit values (e.g., "1h30m")

4. Godoc Comment (Low Priority)

The Duration type should have a more detailed godoc comment explaining its purpose:

// Duration wraps time.Duration to enable JSON unmarshaling from string
// representations in Go duration format (e.g., "5m", "30s", "1h").
//
// This allows configuration files to use human-readable duration strings
// instead of numeric nanosecond values, matching the config-schema.json
// specification.
type Duration struct {
	time.Duration
}

5. Export Consideration (Very Low Priority)

Since Duration is exported, consider if it should be in a more general package if other parts of the codebase might benefit from it. However, keeping it in confpar is reasonable for now.

🔒 Security Considerations

No security concerns identified. The time.ParseDuration function is safe and doesn't introduce vulnerabilities.

⚡ Performance Considerations

Performance impact is negligible. The ParseDuration call happens only during config loading, not in hot paths.

📋 Best Practices Alignment

The code follows Go conventions and matches the project's style based on .golangci.yml requirements. The test file structure is appropriate.

🎯 Recommendation

Approve with minor enhancements. The core functionality is solid, but I suggest:

  1. Add numeric fallback for backwards compatibility (medium priority)
  2. Handle empty strings per schema description (low priority)
  3. Enhance test coverage for edge cases (low priority)

The PR can be merged as-is if backwards compatibility with numeric duration values isn't a concern for your deployment.

@claude
Copy link
Contributor

claude bot commented Dec 18, 2025

Code Review for PR #1547

Thank you for this contribution! This PR addresses an important bug where the IdleTimeout and Timeout (in AccessesWebhook) parameters couldn't be parsed from JSON strings as documented in the schema.

✅ Positive Aspects

  1. Correct Problem Identification: The fix properly addresses the mismatch between the JSON schema (which specifies strings like "5m", "30s") and the Go code (which expected numeric durations).

  2. Clean Custom Type Implementation: The Duration wrapper type is well-designed:

    • Embeds time.Duration for easy access to duration methods
    • Implements both MarshalJSON and UnmarshalJSON for bidirectional conversion
    • Uses standard Go duration parsing (time.ParseDuration)
  3. Good Test Coverage: The test file confpar_test.go covers:

    • Multiple duration formats (seconds, minutes, hours)
    • Zero duration edge case
    • Invalid input handling
    • Both marshal and unmarshal operations
  4. Minimal Code Changes: The fix is surgical - only changes what's necessary and updates usage in server.go to access .Duration field.

🔍 Issues & Suggestions

1. Breaking Change - Migration Path (High Priority)

This is a breaking change for existing configurations that may have numeric values for idle_timeout. Consider:

  • Adding backward compatibility to accept both numeric (nanoseconds) and string formats
  • Documenting the migration path in release notes
  • Example migration code:
func (d *Duration) UnmarshalJSON(b []byte) error {
    // Try parsing as string first (new format)
    var s string
    if err := json.Unmarshal(b, &s); err == nil {
        d.Duration, err = time.ParseDuration(s)
        return err
    }
    
    // Fallback to numeric format (old format, deprecated)
    var n int64
    if err := json.Unmarshal(b, &n); err != nil {
        return err
    }
    d.Duration = time.Duration(n)
    return nil
}

2. Schema Consistency (Medium Priority)

The accesses_webhook.timeout field also changed to Duration type, but there's no corresponding entry in config-schema.json. Consider:

  • Adding schema definition for accesses_webhook to match the code
  • Ensuring all configuration options are documented

3. Test Improvements (Low Priority)

The tests could be enhanced:

  • Add test for backward compatibility (if implemented)
  • Test fractional values like "1.5h" or "100ms"
  • Test negative durations (should probably error)
  • Use table-driven tests more consistently (you already do this well)

Example additional test case:

{"1h30m", 90 * time.Minute, false},
{"100ms", 100 * time.Millisecond, false},
{"-5m", 0, true}, // negative durations should error

4. Code Quality (Minor)

In confpar_test.go:24, consider using require.NoError or handling the path more cleanly:

have, err := json.Marshal(&Duration{item.duration})
require.NoError(t, err, "json.Marshal() failed")

Also, confpar_test.go:45 could be clearer:

if item.wantErr {
    require.Error(t, err, "expected error for input: %s", item.input)
} else {
    require.NoError(t, err, "unexpected error for input: %s", item.input)
}

🔒 Security Considerations

No security concerns identified. The use of standard library time.ParseDuration is safe.

⚡ Performance Considerations

Negligible performance impact. The custom unmarshaling adds minimal overhead during configuration loading (one-time operation).

📋 Recommendations

Before merging:

  1. ✅ Add backward compatibility for existing numeric configurations (see suggestion above)
  2. ✅ Update config-schema.json to include accesses_webhook definition
  3. ✅ Add release notes warning about the configuration format change
  4. Consider: Add example config files showing the new string format

Nice to have:

  • Enhance test coverage per suggestions above
  • Run golangci-lint run to ensure code quality standards are met

📚 Documentation

Ensure the documentation or README is updated to show:

  • Example configurations using string format ("5m", "30s")
  • Migration guide for users with existing configs

Overall, this is a well-implemented fix that solves a real problem. The main concern is ensuring backward compatibility for existing deployments. With the suggested improvements, this will be ready to merge.

Reference: config-schema.json:63-74 shows the expected string format.

@claude
Copy link
Contributor

claude bot commented Dec 18, 2025

Code Review - PR #1547: Parse IdleTimeout parameter from string

Thank you for this fix! This PR correctly addresses the mismatch between the JSON schema (which specifies idle_timeout as a string) and the Go code (which was trying to unmarshal it as a numeric time.Duration).

✅ Strengths

  1. Correct Problem Identification: The schema at config-schema.json:63-74 clearly defines idle_timeout as a string type with Go duration format examples ("5m", "30s", "1h"), but the code was treating it as a numeric value.

  2. Clean Implementation: The Duration wrapper type with custom MarshalJSON/UnmarshalJSON methods is a standard Go idiom for this exact use case.

  3. Good Test Coverage: The new confpar_test.go tests both marshaling and unmarshaling with multiple test cases including edge cases (0s, invalid input).

  4. Consistent Application: Updated both IdleTimeout and AccessesWebhook.Timeout fields, along with the usage site in server/server.go:204.

🔍 Issues & Suggestions

1. Breaking Change Consideration (Minor)

This PR changes how existing numeric values in config files are parsed. If any users have configs with numeric idle_timeout values (which would have worked before with direct JSON number unmarshaling), those configs will now fail. Consider:

  • Documenting this as a breaking change in the PR description
  • Adding a note about config migration in release notes

2. Missing Embedded Field Access (Bug - Critical)

The Duration type embeds time.Duration, but in server/server.go:98, the code accesses conf.IdleTimeout.Seconds() directly. After your changes, this should be conf.IdleTimeout.Duration.Seconds() to access the embedded field.

Current code:

IdleTimeout: int(conf.IdleTimeout.Seconds()),

After your PR (line 68 in confpar.go), this needs to be:

IdleTimeout: int(conf.IdleTimeout.Duration.Seconds()),

You only fixed this for AccessesWebhook.Timeout (line 204 in server.go) but missed the IdleTimeout usage at line 98. This will cause a compilation error.

3. Test Edge Cases (Enhancement)

Consider adding test cases for:

  • Empty string input: ""
  • Negative durations (if they should be rejected)
  • Very large durations
  • Mixed units: "1h30m45s"

Example addition to TestDurationUnmarshalJSON:

{"1h30m", 90 * time.Minute, false},
{"", 0, true}, // or false if empty should default to 0

4. Documentation (Enhancement)

The Duration type would benefit from a doc comment explaining when/why to use it:

// Duration wraps time.Duration to allow unmarshaling from JSON strings
// in Go duration format (e.g., "5m", "30s", "1h"). This matches the
// JSON schema definition which specifies duration fields as strings.

🔒 Security

No security concerns identified. The use of time.ParseDuration is safe and handles malformed input appropriately.

⚡ Performance

Negligible impact - the parsing only happens during config load/reload.

📋 Action Items

Must fix before merge:

  • Fix server/server.go:98 to use conf.IdleTimeout.Duration.Seconds()
  • Verify the code compiles: go build -v ./...
  • Run tests with race detection: go test -race -v ./...

Nice to have:

  • Add test cases for edge cases mentioned above
  • Document as breaking change if applicable
  • Run golangci-lint run to ensure code quality standards

Summary

This is a well-structured fix for a real bug. The primary issue is the missed update to server/server.go:98 which will prevent compilation. Once that's fixed, this should be good to merge!


Review performed following repository standards from CLAUDE.md

@claude
Copy link
Contributor

claude bot commented Dec 19, 2025

Code Review for PR #1547: Fix IdleTimeout parameter parsing

I've reviewed this PR and overall it's a well-implemented fix that addresses a real issue with JSON unmarshaling of duration fields. Here's my detailed feedback:

✅ Strengths

  1. Problem well understood: The PR correctly identifies that time.Duration doesn't unmarshal from JSON strings by default - it expects integers (nanoseconds). The JSON schema specifies strings like "5m", so this fix is necessary.

  2. Good implementation pattern: The custom Duration wrapper type with MarshalJSON/UnmarshalJSON methods is the standard Go pattern for this use case.

  3. Comprehensive test coverage: The new confpar_test.go includes:

    • Both marshal and unmarshal tests
    • Multiple duration formats (seconds, minutes, hours)
    • Error case handling
    • Edge cases (0s)
  4. Consistency: Applied the fix to both IdleTimeout and AccessesWebhook.Timeout fields, maintaining consistency across the codebase.

  5. Backward compatible: The change preserves the existing behavior while enabling string parsing from JSON.

📋 Code Quality

Excellent:

  • Clean, idiomatic Go code
  • Proper error handling in UnmarshalJSON
  • Well-structured tests following Go conventions
  • Minimal changes to existing code

Minor observations:

  • The implementation is straightforward and maintainable
  • Test coverage is good for a configuration package
  • Follows the project's coding standards

🔍 Potential Issues / Considerations

  1. Missing JSON schema for webhook timeout (Minor)

    • The idle_timeout field has proper schema documentation in config-schema.json (lines 63-74)
    • However, accesses_webhook is not in the schema at all
    • Consider adding schema documentation for the webhook feature in a future PR
  2. Error messages (Minor enhancement opportunity)

    • The UnmarshalJSON function returns the raw time.ParseDuration error
    • Consider wrapping errors with context for better debugging:
    func (d *Duration) UnmarshalJSON(b []byte) error {
        var s string
        if err := json.Unmarshal(b, &s); err != nil {
            return fmt.Errorf("duration must be a string: %w", err)
        }
        d.Duration, err = time.ParseDuration(s)
        if err != nil {
            return fmt.Errorf("invalid duration format %q: %w", s, err)
        }
        return nil
    }

    However, this is optional and the current implementation is perfectly acceptable.

  3. Embedded field semantics (Informational)

    • The Duration type embeds time.Duration, which means all time.Duration methods are promoted
    • This is the intended design, but be aware that methods like String() are available on the wrapper
    • This is actually good - it means .Duration access in server/server.go:204 works cleanly

🔒 Security Considerations

No security concerns identified. The changes:

  • Only affect configuration parsing
  • Use standard library functions (time.ParseDuration, json.Marshal/Unmarshal)
  • Don't introduce any injection vulnerabilities
  • Properly handle invalid input with errors

⚡ Performance Considerations

No performance concerns. The changes:

  • Only affect startup/config loading (not hot path)
  • ParseDuration is efficient
  • No allocations beyond what's necessary for JSON parsing

🧪 Testing Recommendations

Current tests are good, but consider adding:

  1. Integration test - Test actual JSON config unmarshaling:
func TestContentUnmarshal(t *testing.T) {
    jsonData := `{"idle_timeout": "5m"}`
    var content Content
    err := json.Unmarshal([]byte(jsonData), &content)
    if err != nil {
        t.Fatalf("unmarshal failed: %v", err)
    }
    if content.IdleTimeout.Duration != 5*time.Minute {
        t.Errorf("got %v, want 5m", content.IdleTimeout)
    }
}
  1. Round-trip test - Verify marshal → unmarshal preserves values

However, the current test coverage is sufficient for merging - these are just nice-to-haves.

📝 Documentation

The code is self-documenting with good comments. The godoc comment on the Duration type clearly explains its purpose.

✨ Final Recommendation

APPROVE with minor suggestions. This PR:

The suggested improvements are all optional enhancements - the code is production-ready as-is.

Great work! 🎉


Review based on repository's CLAUDE.md guidelines and Go best practices.

@claude
Copy link
Contributor

claude bot commented Dec 19, 2025

Code Review: fix(conf): parse IdleTimeout parameter from string

Summary

This PR fixes the JSON unmarshaling of duration parameters (IdleTimeout and AccessesWebhook.Timeout) to properly parse string values in Go duration format (e.g., "5m", "30s", "1h") as described in the schema.


Strengths

  1. Correct Problem Identification: The schema clearly specifies these fields should be strings in Go duration format, but the code was using time.Duration which unmarshals from JSON numbers (nanoseconds), not strings.

  2. Clean Implementation: The custom Duration type wrapper is a standard Go pattern for handling custom JSON marshaling/unmarshaling.

  3. Excellent Test Coverage: The new test file confpar_test.go provides comprehensive coverage:

    • Tests both marshaling and unmarshaling
    • Tests multiple duration values (seconds, minutes, hours)
    • Tests edge cases (0s, invalid input)
    • Tests error handling
  4. Consistent Fix: Both IdleTimeout and AccessesWebhook.Timeout are fixed consistently.

  5. Backward Compatibility: The fix properly handles the Duration accessor in server/server.go:98 and server/server.go:204 by accessing the embedded .Duration field.


🔍 Code Quality Observations

  1. Good: The Duration type embeds time.Duration, allowing direct access to all time.Duration methods.

  2. Good: Error handling in UnmarshalJSON properly returns parsing errors.

  3. Good: The marshaling returns the string representation, maintaining consistency.


⚠️ Potential Issues

  1. Missing Test Case: Consider adding a test for empty string input ("") to verify behavior:

    {"", 0, false},  // or should this error?
  2. Schema Alignment: Verify that the accesses_webhook.timeout field in the schema also specifies type string with Go duration format. I only confirmed this for idle_timeout.

  3. Migration Consideration: Existing configuration files with numeric values will break after this change. Consider:

    • Documenting this as a breaking change
    • Adding a fallback parser that tries numeric nanoseconds if string parsing fails
    • Providing migration guidance in release notes

    Example fallback approach:

    func (d *Duration) UnmarshalJSON(b []byte) error {
        var s string
        if err := json.Unmarshal(b, &s); err == nil {
            d.Duration, err = time.ParseDuration(s)
            return err
        }
        
        // Fallback: try numeric nanoseconds for backward compatibility
        var ns int64
        if err := json.Unmarshal(b, &ns); err == nil {
            d.Duration = time.Duration(ns)
            return nil
        }
        
        return fmt.Errorf("invalid duration format")
    }

🔒 Security Considerations

  • ✅ No security concerns identified
  • The time.ParseDuration function is safe and does not introduce vulnerabilities

Performance Considerations

  • ✅ Minimal performance impact
  • String parsing happens only during config loading, not in hot paths
  • The embedded struct approach has zero runtime overhead after unmarshaling

🧪 Testing Recommendations

  1. ✅ Unit tests are excellent and comprehensive

  2. ⚠️ Consider adding integration tests:

    • Load a sample config file with string duration values
    • Verify the server starts correctly
    • Verify idle timeout actually works with parsed values
  3. ⚠️ Add a test case for the webhook timeout usage in server.go:204


📋 Suggested Changes

Optional - Add backward compatibility:

 func (d *Duration) UnmarshalJSON(b []byte) (err error) {
 	var s string
-	if err = json.Unmarshal(b, &s); err == nil {
-		d.Duration, err = time.ParseDuration(s)
+	if err = json.Unmarshal(b, &s); err != nil {
+		// Fallback: try numeric value for backward compatibility
+		var ns int64
+		if err = json.Unmarshal(b, &ns); err != nil {
+			return err
+		}
+		d.Duration = time.Duration(ns)
+		return nil
 	}
-	return
+	d.Duration, err = time.ParseDuration(s)
+	return err
 }

Required - Add test for empty string:

{"", 0, false},  // Verify empty string behavior

Overall Assessment

This is a well-implemented fix that correctly addresses the mismatch between the schema specification and code implementation. The addition of comprehensive tests is commendable.

Recommendation: ✅ Approve with minor suggestions

  • Consider adding backward compatibility for existing configs
  • Add empty string test case
  • Document as potentially breaking change in release notes

Code Quality: 9/10
Test Coverage: 9/10
Documentation: 8/10 (could use migration notes)

@claude
Copy link
Contributor

claude bot commented Dec 26, 2025

Code Review for PR #1547: Fix IdleTimeout parameter parsing

Thank you for this PR! This is a well-thought-out fix that addresses the mismatch between the JSON schema (which specifies a string format) and the Go struct field (which uses time.Duration).

Positive Aspects

Correct Problem Identification: The PR properly identifies that the schema defines idle_timeout as a string in Go duration format (e.g., "5m", "30s"), but the struct was trying to unmarshal it as a time.Duration directly, which doesn't work with JSON strings.

Good Solution Pattern: The custom Duration type wrapper is a clean, idiomatic Go solution that properly implements both MarshalJSON and UnmarshalJSON.

Comprehensive Test Coverage: The new test file confpar_test.go includes both marshal and unmarshal tests with multiple test cases, including edge cases (0s duration) and error handling (invalid input).

Consistent Application: The fix is applied to both IdleTimeout and AccessesWebhook.Timeout, ensuring consistency across the codebase.

Proper Usage Update: The change in server/server.go:201 correctly accesses the wrapped Duration field.

Issues & Concerns

1. Missing Integration Test or Example

The schema documents example values like "5m", "30s", but there's no sample configuration file demonstrating this feature. Consider adding a sample config that includes idle_timeout.

Suggestion: Add an example to config/samples/ directory:

{
  "idle_timeout": "5m",
  "accesses": []
}

2. Test Case: Marshal/Unmarshal Round-trip

While the tests cover marshal and unmarshal separately, they don't test a complete round-trip (marshal → unmarshal → verify equality). This is important for ensuring data integrity.

Suggestion: Add a round-trip test:

func TestDurationRoundTrip(t *testing.T) {
    original := Duration{5 * time.Minute}
    marshaled, err := json.Marshal(original)
    if err != nil {
        t.Fatalf("marshal failed: %v", err)
    }
    var decoded Duration
    if err := json.Unmarshal(marshaled, &decoded); err != nil {
        t.Fatalf("unmarshal failed: %v", err)
    }
    if decoded.Duration != original.Duration {
        t.Fatalf("round-trip failed: got %v, want %v", decoded, original)
    }
}

3. Documentation: Nil/Zero Value Handling

The code doesn't document what happens when the JSON field is omitted entirely (nil/zero value). Currently, it will default to 0s, which is fine, but this should be documented.

Suggestion: Add a comment explaining zero-value behavior:

// Duration wraps time.Duration to allow unmarshaling from JSON strings
// in Go duration format (e.g., "5m", "30s", "1h").
// Zero value (omitted field or empty string) results in 0s duration.

4. Error Handling: Unmarshal Failure Message

When UnmarshalJSON fails, the error from time.ParseDuration is returned directly. This might not be user-friendly for configuration errors.

Consider: Wrapping the error with context:

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return fmt.Errorf("duration must be a string: %w", err)
    }
    var err error
    d.Duration, err = time.ParseDuration(s)
    if err != nil {
        return fmt.Errorf("invalid duration format %q: %w", s, err)
    }
    return nil
}

5. Potential Breaking Change

If anyone was using numeric values for idle_timeout (though unlikely since it wouldn't have worked properly), this is technically a breaking change. The schema already specifies string format, so this is correct, but worth noting in release notes.

6. Empty String Handling

The code should explicitly test what happens with an empty string "". According to Go's time.ParseDuration, this returns an error, but it should be verified in tests.

Suggestion: Add test case:

{`"", 0, true}, // empty string should error

Performance Considerations

✅ No performance concerns. The custom marshaling/unmarshaling adds negligible overhead and is only called during config loading.

Security Considerations

✅ No security issues identified. The time.ParseDuration function is safe and doesn't introduce any injection vulnerabilities.

Code Quality & Best Practices

According to CLAUDE.md:

Recommendations Summary

Must Fix (before merge):

  • None - the code is functional as-is

Should Fix (strongly recommended):

  1. Add round-trip test case
  2. Add test case for empty string
  3. Improve error messages with context

Nice to Have:

  1. Add example configuration file
  2. Document zero-value behavior more clearly

Overall Assessment

This is a solid PR that correctly fixes a real bug. The implementation is clean, well-tested, and follows Go best practices. With minor improvements to error handling and test coverage, this would be production-ready.

Recommendation: ✅ Approve with minor suggestions

The code can be merged as-is, but implementing the "Should Fix" items would make it more robust and maintainable.

@fclairamb fclairamb enabled auto-merge (squash) December 26, 2025 20:52
@claude
Copy link
Contributor

claude bot commented Dec 26, 2025

Code Review

Thank you for this fix! This PR correctly addresses the mismatch between the JSON schema (which expects string duration values like "5m", "30s") and the Go code (which was using time.Duration directly, expecting numeric nanoseconds).

✅ Strengths

  1. Clean solution: The custom Duration wrapper type with UnmarshalJSON/MarshalJSON methods is the idiomatic Go approach for this problem.

  2. Comprehensive coverage: The PR correctly identifies and fixes both duration fields:

    • IdleTimeout in Content
    • Timeout in AccessesWebhook
  3. Good test coverage: The test file includes:

    • Multiple duration formats (seconds, minutes, hours)
    • Zero duration edge case
    • Invalid input error handling
    • Both marshal and unmarshal operations
  4. Backward compatibility: Existing code using .Duration (e.g., conf.IdleTimeout.Seconds() in server/server.go:98) continues to work seamlessly since the embedded time.Duration field is promoted.

🔍 Minor Observations

  1. Error message: In confpar_test.go:45, the error message could be more specific:

    t.Fatalf("expecting error for invalid input")

    Could be:

    t.Fatalf("expected error for invalid input %q, got nil", item.input)
  2. Test table naming: Using have/want is good, but item could be more descriptive as tc (test case) or tt (test table), following common Go conventions.

  3. Export consideration: The Duration type is unexported in the package but used in exported structs. This is fine since it's embedded and the time.Duration field is promoted, but you might consider adding a godoc comment explaining its purpose for future maintainers.

🎯 Suggestions (Optional)

Consider adding a test case for the roundtrip marshaling to ensure symmetry:

func TestDurationRoundtrip(t *testing.T) {
    original := Duration{5 * time.Minute}
    data, err := json.Marshal(&original)
    if err != nil {
        t.Fatalf("marshal: %v", err)
    }
    
    var decoded Duration
    if err := json.Unmarshal(data, &decoded); err != nil {
        t.Fatalf("unmarshal: %v", err)
    }
    
    if decoded.Duration != original.Duration {
        t.Fatalf("roundtrip failed: got %v, want %v", decoded, original)
    }
}

📋 Code Quality

  • ✅ Follows Go conventions and idioms
  • ✅ No golangci-lint issues expected
  • ✅ Properly uses embedded types
  • ✅ Handles errors appropriately
  • ✅ Includes unit tests

🔒 Security & Performance

  • ✅ No security concerns
  • ✅ No performance impact (marshaling overhead is negligible)
  • ✅ Proper error handling prevents panics

Overall Assessment

This is a well-implemented fix that correctly addresses the schema mismatch. The code is clean, well-tested, and maintains backward compatibility. The minor suggestions above are optional improvements and don't block merging.

Recommendation: Approve and merge

@claude
Copy link
Contributor

claude bot commented Dec 26, 2025

Code Review: fix(conf): parse IdleTimeout parameter from string

Thank you for this fix! This PR correctly addresses the mismatch between the JSON schema (which expects string values like "5m") and the Go code (which was trying to unmarshal directly into time.Duration). Here's my detailed review:

✅ Strengths

  1. Correct Problem Identification: The PR properly fixes the issue where idle_timeout in the JSON schema is defined as a string, but the code was unmarshaling it as a duration directly.

  2. Comprehensive Test Coverage: Excellent addition of confpar_test.go with both marshal and unmarshal tests. This brings much-needed test coverage to the config package.

  3. Consistent Pattern: The Duration wrapper type is applied consistently to both IdleTimeout and AccessesWebhook.Timeout, which both need string parsing.

  4. Backward Compatibility: The implementation should maintain backward compatibility for existing configurations.

🔍 Code Quality Observations

Good:

  • Clean, focused implementation following Go idioms
  • Proper error handling in UnmarshalJSON
  • Test cases cover edge cases (0s, invalid input)
  • Minimal surface area changes

Minor Suggestions:

  1. Test Case Enhancement (config/confpar/confpar_test.go:23-26):
    Consider using require.NoError or similar for better test failure messages:

    have, err := json.Marshal(&Duration{item.duration})
    if err != nil {
        t.Fatalf("json.Marshal() for %v: %v", item.duration, err)
    }
  2. Test Organization (config/confpar/confpar_test.go):
    The test cases look good, but you might want to add a test case for the complete Content struct to ensure the integration works end-to-end:

    func TestContentUnmarshalWithDuration(t *testing.T) {
        jsonStr := `{"idle_timeout": "5m"}`
        var c Content
        if err := json.Unmarshal([]byte(jsonStr), &c); err != nil {
            t.Fatalf("failed to unmarshal: %v", err)
        }
        if c.IdleTimeout.Duration != 5*time.Minute {
            t.Fatalf("have:%v want:%v", c.IdleTimeout.Duration, 5*time.Minute)
        }
    }
  3. Error Message (config/confpar/confpar.go:91-94):
    The current implementation silently returns parse errors. Consider wrapping the error for better debugging:

    func (d *Duration) UnmarshalJSON(b []byte) error {
        var s string
        if err := json.Unmarshal(b, &s); err != nil {
            return fmt.Errorf("failed to unmarshal duration: %w", err)
        }
        var err error
        d.Duration, err = time.ParseDuration(s)
        if err != nil {
            return fmt.Errorf("invalid duration format %q: %w", s, err)
        }
        return nil
    }

🔒 Security Considerations

No security concerns identified. The time.ParseDuration function is safe and doesn't introduce any vulnerabilities.

⚡ Performance Considerations

The performance impact is negligible. String parsing happens only during configuration loading, which is infrequent.

📋 Testing Recommendations

  • ✅ Unit tests added for marshal/unmarshal
  • ⚠️ Consider adding an integration test that loads a sample config with string durations
  • ⚠️ Verify that existing config files with duration strings work correctly

🎯 Overall Assessment

This is a solid fix that properly addresses issue #1519. The code is clean, well-tested, and follows Go best practices. The minor suggestions above are optional improvements.

Recommendation: ✅ Approve with optional minor enhancements


Review performed by Claude Code following repository conventions from CLAUDE.md

@fclairamb fclairamb merged commit 73eff76 into fclairamb:main Dec 26, 2025
2 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