Billionmail/BillionMail · error

end_time must greater than start_time

Error message

end_time must greater than start_time

What it means

TaskStatService.filterAndPrepareTimeSection validates the chart time window used by GetTaskStatChart. If startTime exceeds endTime it panics with a localized message via public.Lang, because a negative or inverted range would produce meaningless chart buckets. Note the odd guard: endTime is only defaulted to now when endTime < 0, so an unset (0) endTime does not protect against inversion.

Source

Thrown at core/internal/service/batch_mail/stat_service.go:67

	return map[string]interface{}{
		"dashboard":         s.getTaskDashboard(taskId, domain, startTime, endTime),
		"mail_providers":    s.getTaskMailProviders(taskId, domain, startTime, endTime),
		"send_mail_chart":   s.getTaskSendMailChart(taskId, domain, startTime, endTime),
		"bounce_rate_chart": s.getTaskBounceRateChart(taskId, domain, startTime, endTime),
		"open_rate_chart":   s.getTaskOpenRateChart(taskId, domain, startTime, endTime),
		"click_rate_chart":  s.getTaskClickRateChart(taskId, domain, startTime, endTime),
	}
}

// filterAndPrepareTimeSection
func (s *TaskStatService) filterAndPrepareTimeSection(startTime, endTime int64) (int64, int64) {
	if startTime > 0 && endTime < 0 {
		endTime = time.Now().Unix()
	}

	if startTime > endTime {
		panic(public.Lang("end_time must greater than start_time"))
	}

	return startTime, endTime
}

// prepareChartData
func (s *TaskStatService) prepareChartData(startTime, endTime int64) (string, string) {
	columnType := "daily"
	secs := endTime - startTime

	xAxisField := "EXTRACT(EPOCH FROM date_trunc('day', to_timestamp(sm.log_time_millis / 1000)))::bigint as x"
	if secs < 86400 {
		columnType = "hourly"
		xAxisField = "to_char(to_timestamp(sm.log_time_millis / 1000), 'HH24')::integer as x"
	}

	return columnType, xAxisField
}

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Fix the caller to pass startTime <= endTime in Unix seconds.
  2. Normalize units before calling (divide ms timestamps by 1000).
  3. Validate/swapon the UI side: require an end date and enforce end >= start in the form.
  4. Harden filterAndPrepareTimeSection to default endTime to now when endTime <= 0, since 0 is the common 'unset' value.

Example fix

// before
if startTime > 0 && endTime < 0 {
    endTime = time.Now().Unix()
}
// after
if startTime > 0 && endTime <= 0 {
    endTime = time.Now().Unix()
}
Defensive patterns

Strategy: validation

Validate before calling

// Go: caller-side guard before GetTaskStatChart
if start > 0 && (end <= 0 || start > end) {
    return fmt.Errorf("invalid range: start=%d end=%d", start, end)
}

Type guard

func validTimeRange(start, end int64) bool {
    return start > 0 && end > 0 && start <= end
}

Try / catch

// the service panics, so recover at the handler boundary
defer func() {
    if r := recover(); r != nil {
        response.Fail(ctx, fmt.Sprintf("%v", r))
    }
}()
svc.GetTaskStatChart(ctx, start, end)

Prevention

When it happens

Trigger: Calling GetTaskStatChart with startTime > endTime, e.g. startTime=1700000000, endTime=0 (endTime only auto-set when negative), or the caller swapping the two parameters.

Common situations: Frontend sends start/end in milliseconds while the backend expects Unix seconds (one value dwarfs the other); an empty end-date field submitted as 0; date pickers allowing end before start.

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/cbea25037635d140. Report an issue: GitHub.