skip to content

A Go webhook parses partner dates with layout "01/02/2006", but the partner sends day-first. Why do the bugs appear only after the 12th?

level: seniorimportance: should knowfreq 44%

answer

  1. Both elements are legal, so both layouts parse
  2. Twelve is the number that matters
  3. The calendar starts the failures, not a deploy
  4. Silent early in the month, loud later
  5. A fixture dated the 4th proves nothing

basics

~20 s

In a Go layout 01 is the month and 02 the day. Days of 12 or under are valid either way, so Parse succeeds and stores the wrong date. From the 13th the month is out of range and Parse errors.

solid answer

~50 s

`01` is the month element and `02` the day, so `"01/02/2006"` reads a month-first date. Fed a day-first value, every date from the 1st to the 12th still parses — `03/04/2024` is a perfectly good March 4th under that layout, even though the sender meant April 3rd — so the receiver stores wrong data with no error. On the 13th the leading field is no longer a valid month, and `time.Parse` returns a `*time.ParseError` reporting the month out of range. The symptom is therefore a monthly rhythm: silently corrupt records early in the month, a burst of parse failures from the 13th. The cure is not a smarter parser but the boundary contract: require an offset-bearing RFC 3339 timestamp, record each sender's layout explicitly rather than inferring it, and pin a per-sender fixture whose day is above 12 so the swap can never pass a test.

code

go · 7 lines
go
// Sender means 4 April 2024; the layout says month first.
t, _ := time.Parse("01/02/2006", "03/04/2024")
fmt.Println(t.Format(time.DateOnly)) // 2024-03-04 — March 4th, stored without complaint

// Same sender, later in the month.
_, err := time.Parse("01/02/2006", "25/03/2024")
fmt.Println(err != nil) // true — 25 is not a valid month

go deeper

for a junior

Remember which element is which: 01 is the month, 02 the day. Know that mixing them up does not always produce an error, so a passing parse is not proof the date is right.

for a middle

Explain why days 1 to 12 parse under either reading and what error the 13th produces, and describe how to read the parse error to find the offending element.

for a senior

Diagnose from the symptom pattern - silent corruption then a mid-month error burst - and fix at the boundary: an explicit layout per sender, fixtures pinned above the 12th, loud rejection rather than substituted values.

for a principal

Decide the policy rather than the patch: what the integration contract requires, whether ambiguous formats are accepted at all, and who carries the cost of migrating a partner off one.

## What actually happens Go layouts are example-based, and `01` and `02` are both legal elements: `01` is the month (January) and `02` the day (the 2nd). Swap them and you have a different, equally valid layout. `time.Parse` has no way to know which one the sender meant; it simply reads the first field as a month and the second as a day. For any day-of-month from 1 to 12, the swapped reading is a real date. `"03/04/2024"` under `"01/02/2006"` is March 4th; under `"02/01/2006"` it is April 3rd. Both parse, neither errors, and the value that reaches your database is off by however far apart those two dates are. From the 13th onwards the first field exceeds 12, the month range check fails, and Parse returns a `*time.ParseError` whose message reports the month as out of range. ## The failure signature This is why the bug has a calendar rhythm, and why it is usually reported by someone else: - Days 1–12: no errors, no alerts, wrong data. Records are written with a date that is plausible, in range, and simply not what the sender sent. - Days 13–31: a steady stream of parse errors from one sender. Whoever is on call sees a sudden "bad payload" rate and, if the error is being swallowed and counted rather than surfaced, may see nothing but a drop in ingested volume. The two symptoms look unrelated, which is exactly what makes the diagnosis take longer than it should. The tell is that the failures start on a date, not at a deploy. ## Reading the error `time.Parse` returns `*time.ParseError`, and it is worth knowing what it carries: `Layout` and `Value` (the whole layout and input), `LayoutElem` and `ValueElem` (the specific element it choked on and the text it was looking at), and `Message`. For a mismatched element the rendered error names the layout element, in the shape `cannot parse "…" as "01"` — which points straight at the offending piece rather than the whole string. For a value that parsed but was out of bounds, the message is a range complaint such as the month being out of range. Use `errors.As` to reach the struct when you want to log `LayoutElem` as a field, and always log the sender identity next to it; "which partner" is the question you will actually need answered. ## Why guessing is not a fix The tempting repair is a loop over candidate layouts, returning the first that parses. That is strictly worse than being wrong in one direction, because it is wrong *non-deterministically*: `03/04/2024` matches the first candidate in the list, `25/03/2024` falls through to the second, and now a single sender's data has two different interpretations depending on the day of the month. Format detection cannot succeed where the format is genuinely ambiguous — the information is not in the string. The same objection applies to sniffing a sample of a partner's traffic. A sample drawn in the first twelve days of a month proves nothing at all. ## What actually prevents it 1. **Make the wire format carry the answer.** RFC 3339 fixes field order and includes the offset. If you can put that in the integration contract, the whole class of bug disappears; every conversion argument after that is about who does the converting. 2. **Record the layout per sender, explicitly.** One registry entry per partner, alongside their credentials and endpoint, reviewed when the integration is onboarded. Never infer it, and never share one layout across senders because two of them happened to agree. 3. **Pin a fixture per sender to a reference instant that cannot be ambiguous.** Choose a day above the 12th, a non-zero offset, and a non-zero fractional second, then assert the resulting instant exactly. A swapped layout fails that test immediately; a fixture dated the 4th of the month passes it forever. 4. **Fail loudly at the edge.** A payload whose timestamp does not parse is a rejected payload with an error naming the expected layout, not a silently substituted `time.Now()` and not a dropped field. The whole value of a strict boundary is that the error arrives while someone can still correlate it with a change. 5. **Prefer the named constants in code.** `time.RFC3339` and `time.DateOnly` cannot be typed wrong; `"01/02/2006"` and `"02/01/2006"` differ by two characters and read identically in review.

  • When a parse does fail, how do you find which part of the layout was wrong?
    `time.Parse` returns a `*time.ParseError`. Pull it out with `errors.As` and it gives you `Layout` and `Value` for the whole strings plus `LayoutElem` and `ValueElem` for the element it stopped on, so the log line can name the exact element rather than dumping the input. Log the sender identity beside it — that is the field you will actually filter on.
  • Two partners send different date formats. Why not just try each layout until one parses?
    Because ambiguous values match more than one candidate, so the result depends on list order and on the day of the month. The same partner's data ends up interpreted two ways, and the bug is now non-deterministic rather than merely wrong. Keep an explicit layout per sender in configuration and reject anything that does not match it.
  • What would you put in a regression test so this can never come back?
    A per-sender fixture pinned to an instant that a swapped layout cannot satisfy: a day above the 12th, a non-zero offset and a non-zero fractional second, asserted against an exact expected `time.Time`. Add a negative case with the other sender's format and assert it errors, so a permissive fallback added later breaks the build.
  • The receiver currently substitutes time.Now() when a timestamp will not parse. What is wrong with that?
    It converts a loud, correlatable failure into permanently wrong data that looks plausible. Nobody can tell later which rows were substituted, and the ingestion metrics stay green through a real integration break. Reject the payload with an error naming the expected format, and let the sender or the on-call engineer see it while the cause is still recent.

saying these in an interview costs you the question

  • Says a swapped month and day layout always fails to parse
  • Proposes trying layouts in a loop until one succeeds
  • Wants to auto-detect the sender's format from samples
  • Substitutes the current time when a timestamp will not parse
  • Tests only with a fixture dated in the first twelve days