git.iko.soy/parseable-macos
on master help sign in
Fix live tail 400 error by widening query time window (#6)

The Parseable server truncates query time-ranges to minute boundaries. Live tail's 2-second polling window could collapse to a zero-width range after truncation, causing the server to return HTTP 400. This disproportionately affected streams with sparse data (e.g. staging-frontend) where there was no data at the truncated boundary. Fix: ensure the query window is always at least 90 seconds wide using min(idealStart, now - 90s). Fingerprint-based deduplication already prevents duplicate entries. Also surface the actual server error message in userFriendlyMessage instead of discarding it, making future diagnostics easier. https://claude.ai/code/session_01WRvjTknMZhZDVReuayPcMr Co-authored-by: Claude <noreply@anthropic.com>

commit9f30a7fa878a6903f43da9df58590831d6e09d0c authoriko <mail@iko.soy> dateSun 15 Feb 2026, 12:26 (8 months ago) parentedca1ae
3 files changed+30−6
ParseableViewer/Services/ParseableClient.swift+6−2 ParseableViewer/ViewModels/LiveTailViewModel.swift+8−3 ParseableViewerTests/ParseableClientTests.swift+16−1
ParseableViewer/Services/ParseableClient.swift+6−2
@@ -28,8 +28,12 @@ enum ParseableError: LocalizedError {
2828 return "Authentication failed. Check your username and password."
2929 case .invalidURL:
3030 return "The server URL is invalid. Check the format (e.g. https://host:port)."
31− case .serverError(let code, _):
32− return "Server returned error \(code). Check that the URL points to a Parseable instance."
31+ case .serverError(let code, let message):
32+ let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
33+ if trimmed.isEmpty {
34+ return "Server returned error \(code)."
35+ }
36+ return "Server error (\(code)): \(trimmed)"
3337 default:
3438 return parseableError.localizedDescription
3539 }
ParseableViewer/ViewModels/LiveTailViewModel.swift+8−3
@@ -110,9 +110,14 @@ final class LiveTailViewModel {
110110
111111 private func poll(client: ParseableClient, stream: String) async {
112112 let now = Date()
113− let queryStart = lastTimestamp
114− ?? Calendar.current.date(byAdding: .second, value: -30, to: now)
115− ?? now.addingTimeInterval(-30)
113+ // The Parseable server truncates query time-ranges to minute
114+ // boundaries, so a narrow window (< 60 s) can collapse to a
115+ // zero-width range and return HTTP 400. Always look back at
116+ // least 90 seconds; fingerprint deduplication prevents showing
117+ // duplicate entries.
118+ let idealStart = lastTimestamp ?? now.addingTimeInterval(-30)
119+ let safeStart = now.addingTimeInterval(-90)
120+ let queryStart = min(idealStart, safeStart)
116121 let sql = "SELECT * FROM \(QueryViewModel.escapeSQLIdentifier(stream)) ORDER BY p_timestamp DESC LIMIT 200"
117122
118123 do {
ParseableViewerTests/ParseableClientTests.swift+16−1
@@ -242,7 +242,22 @@ final class ParseableClientTests: XCTestCase {
242242 func testUserFriendlyMessageForServerError() {
243243 let msg = ParseableError.userFriendlyMessage(for: ParseableError.serverError(500, "fail"))
244244 XCTAssertTrue(msg.contains("500"))
245− XCTAssertTrue(msg.contains("Parseable"))
245+ XCTAssertTrue(msg.contains("fail"))
246+ }
247+
248+ func testUserFriendlyMessageForServerErrorIncludesDetail() {
249+ let msg = ParseableError.userFriendlyMessage(for: ParseableError.serverError(400, "Stream not found"))
250+ XCTAssertEqual(msg, "Server error (400): Stream not found")
251+ }
252+
253+ func testUserFriendlyMessageForServerErrorEmptyBody() {
254+ let msg = ParseableError.userFriendlyMessage(for: ParseableError.serverError(502, ""))
255+ XCTAssertEqual(msg, "Server returned error 502.")
256+ }
257+
258+ func testUserFriendlyMessageForServerErrorTrimsWhitespace() {
259+ let msg = ParseableError.userFriendlyMessage(for: ParseableError.serverError(400, " bad query \n"))
260+ XCTAssertEqual(msg, "Server error (400): bad query")
246261 }
247262
248263 func testUserFriendlyMessageForInvalidURL() {