Sending timestamp from go and having the timestamp with 2 digits milliseconds fails to decode
Is there a way to configure knit typescript to support timestamp format to allow 2 digits up to 9 digits milliseconds-nanoseconds even when zero trimmed values?
Connect-go correctly encodes timestamp { seconds, nanos } and sends it with 2 digits e.g: 2025-08-23T22:16:09**.55Z**
@go-aegian, so the issue is that the knit-ts client won't accept the value in your example, produced by knit-go? If so, does it accept any number of digits after the decimal point? Or is the issue that it only accepts a full 9 digits? In any event, that is almost certainly a knit-ts issue, and I would advise filing a bug there: https://github.com/bufbuild/knit-ts. But please include more details, such as what kinds of values do work and also include the actual error messages you are seeing.
@go-aegian, I just saw your related issue in the connect-go repo. I also just double-checked the spec for the JSON format of a google.protobuf.Timestamp and it states that the portion after the decimal must have 0, 3, 6, or 9 digits.
So please disregard my earlier suggestion to file a bug in knit-ts. This is not a bug in knit-ts but in the code that produced that string to begin with.
To do that, we need a full repro case. We (knit-go and connect-go) are using the protojson package in the google.golang.org/protobuf module, which should implement this correctly and emit the correct number of digits after the decimal point. While it is possible there is a bug in the protojson implementation, I find it more likely that something else is going awry.
So please include more details about the schema you are using (ideally simplify it to a single service and method with a simple message with only the fields necessary to demonstrate the issue) so that we can figure out where exactly the string is getting mangled or perhaps determine that the bug is in protojson.
Not sure where in your repo is this file, but on my end, it ends up being part of the dist files
\node_modules@bufbuild\knit\node_modules@bufbuild\protobuf\dist\esm\google\protobuf\timestamp_pb.js
function with problem
fromJson(json, options) {
if (typeof json !== "string") {
throw new Error(cannot decode google.protobuf.Timestamp from JSON: ${proto3.json.debug(json)});
}
const matches = json.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:Z|.([0-9]{3,9})Z|([+-][0-9][0-9]:[0-9][0-9]))$/);
if (!matches) {
throw new Error(cannot decode google.protobuf.Timestamp from JSON: invalid RFC 3339 string);
}
const ms = Date.parse(matches[1] + "-" + matches[2] + "-" + matches[3] + "T" + matches[4] + ":" + matches[5] + ":" + matches[6] + (matches[8] ? matches[8] : "Z"));
if (Number.isNaN(ms)) {
throw new Error(cannot decode google.protobuf.Timestamp from JSON: invalid RFC 3339 string);
}
if (ms < Date.parse("0001-01-01T00:00:00Z") || ms > Date.parse("9999-12-31T23:59:59Z")) {
throw new Error(cannot decode message google.protobuf.Timestamp from JSON: must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive);
}
this.seconds = protoInt64.parse(ms / 1000);
this.nanos = 0;
if (matches[7]) {
this.nanos = (parseInt("1" + matches[7] + "0".repeat(9 - matches[7].length)) - 1000000000);
}
return this;
}
This line
const matches = json.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:Z|\.([0-9]{3,9})Z|([+-][0-9][0-9]:[0-9][0-9]))$/);
Needs to be replaced with this instead in that function
const matches = json.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]{1,9}))?(?:Z|([+-][0-9][0-9]:[0-9][0-9]))$/);
As repro here is the test I made with it
const fromJson = (timestamp, json) => {
if (typeof json !== "string") {
throw new Error(cannot decode google.protobuf.Timestamp from JSON: ${json});
}
//Current code that fails
//const matches = json.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:Z|.([0-9]{3,9})Z|([+-][0-9][0-9]:[0-9][0-9]))$/);
// Code that works
const matches = json.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:.([0-9]{1,9}))?(?:Z|([+-][0-9][0-9]:[0-9][0-9]))$/);
if (!matches) {
throw new Error(cannot decode google.protobuf.Timestamp from JSON: invalid RFC 3339 string);
}
const ms = Date.parse(matches[1] + "-" + matches[2] + "-" + matches[3] + "T" + matches[4] + ":" + matches[5] + ":" + matches[6] + (matches[8] ? matches[8] : "Z"));
if (Number.isNaN(ms)) {
throw new Error(cannot decode google.protobuf.Timestamp from JSON: invalid RFC 3339 string);
}
if (ms < Date.parse("0001-01-01T00:00:00Z") || ms > Date.parse("9999-12-31T23:59:59Z")) {
throw new Error(cannot decode message google.protobuf.Timestamp from JSON: must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive);
}
timestamp.seconds = protoInt64.parse(ms / 1000);
timestamp.nanos = 0;
if (matches[7]) {
timestamp.nanos = (parseInt("1" + matches[7] + "0".repeat(9 - matches[7].length)) - 1000000000);
}
}
let timestamp = new Timestamp(); fromJson(timestamp, "2025-09-22T22:16:10.55Z"); console.log("timestamp: ", timestamp.toDate() .toISOString())
Hope this help you, let me know what should I do once you get to it
I think you might be using an old version protobuf-es. The line you state is the problem looks like it was changed eight months ago to support fractional seconds in timestamps: https://github.com/bufbuild/protobuf-es/pull/1061
How so then it's embedded in buf-knit? I think the problem is inside knit which needs to be upgraded
@bufbuild/knit/package.json that shows old versions
https://github.com/bufbuild/knit-ts/blob/main/packages/knit/package.json
{ "name": "@bufbuild/knit", "version": "0.0.7", "description": "TypeScript client for Knit", "license": "Apache-2.0", "repository": { "type": "git", "url": "https://github.com/bufbuild/knit-ts.git", "directory": "packages/knit" }, "sideEffects": false, "type": "module", "main": "./dist/cjs/index.js", "exports": { ".": { "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { "types": "./dist/cjs/index.d.ts", "default": "./dist/cjs/index.js" } }, "./gateway": { "import": { "types": "./dist/esm/gateway/index.d.ts", "default": "./dist/esm/gateway/index.js" }, "require": { "types": "./dist/cjs/gateway/index.d.ts", "default": "./dist/cjs/gateway/index.js" } } }, "typesVersions": { "": { "gateway": [ "./dist/cjs/gateway/index.d.ts" ] } }, "dependencies": { "@bufbuild/protobuf": "^1.5.0", "@connectrpc/connect": "^1.1.3", "@connectrpc/connect-web": "^1.1.3" }, "devDependencies": { "@buf/bufbuild_knit.connectrpc_es": "1.0.0-20230504140941-3dc602456973.1", "@buf/bufbuild_knit.bufbuild_es": "1.3.1-20230504140941-3dc602456973.1", "@jest/globals": "^29.7.0", "tsup": "^8.0.1", "@bufbuild/knit-test-spec": "0.0.0", "tsconfig": "0.0.0", "eslint-config-custom": "0.0.0" }, "files": [ "dist/**" ], "scripts": { "clean": "rm -rf ./dist/ .turbo/*", "lint": "eslint .", "attw": "attw --pack", "test": "NODE_OPTIONS=--experimental-vm-modules ../../node_modules/.bin/jest", "test:watch": "pnpm run build:esm+types --watch & pnpm run test --watchAll", "build:esm": "tsup --format esm", "build:cjs": "tsup --format cjs && mv ./dist/cjs/index.d.cts ./dist/cjs/index.d.ts && mv ./dist/cjs/gateway/index.d.cts ./dist/cjs/gateway/index.d.ts", "build": "pnpm run build:esm && pnpm run build:cjs" } }
On Sun, Sep 28, 2025, 11:08 Joshua Humphries @.***> wrote:
jhump left a comment (bufbuild/knit#65) https://github.com/bufbuild/knit/issues/65#issuecomment-3343688413
I think you might be using an old version protobuf-es. The line you state is the problem looks like it was changed eight months ago to support fractional seconds in timestamps: bufbuild/protobuf-es#1061 https://github.com/bufbuild/protobuf-es/pull/1061
— Reply to this email directly, view it on GitHub https://github.com/bufbuild/knit/issues/65#issuecomment-3343688413, or unsubscribe https://github.com/notifications/unsubscribe-auth/AFIOQ5J3W2VZYIURG3COO2D3U72WTAVCNFSM6AAAAACHMKLWTCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTGNBTGY4DQNBRGM . You are receiving this because you were mentioned.Message ID: @.***>
I don't have any protobuf-es on my end at all, all I have is buf libs, so whatever is failing is on the knit end of their libs and not updated correctly, that might explain why knit is still picking up a function that is failing to get the right version on your end
"@bufbuild/buf": "1.57.2",
"@bufbuild/knit": "0.0.7",
"@bufbuild/protobuf": "2.9.0",
"@connectrpc/connect": "2.1.0",
"@connectrpc/connect-web": "2.1.0",