Skip to content

Conversation

@asarandi
Copy link

@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! 🎉

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant