Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RPC Client does not maintain the Date type #3771

Closed
cybercoder-naj opened this issue Dec 25, 2024 · 8 comments
Closed

RPC Client does not maintain the Date type #3771

cybercoder-naj opened this issue Dec 25, 2024 · 8 comments
Labels

Comments

@cybercoder-naj
Copy link

What version of Hono are you using?

4.5.5

What runtime/platform is your app running on? (with version if possible)

Bun 1.1.38

What steps can reproduce the bug?

// server code
import { Hono } from 'hono';

const app = new Hono()
  .get('/date', async c => {
    const date = new Date();
    const resBody = {
      msg: 'The time is:',
      date: date
    }; // <-- This is typed as {msg: string, date: Date}
    return c.json(resBody, 200);
  })

// client code
const client = hc<typeof app>(BASE_URL);
const response = await client.$get();
const body = await response.json(); // <-- this is typed as {msg: string, date: string}

What is the expected behavior?

The expected behaviour is:

// client code
const client = hc<typeof app>(BASE_URL);
const response = await client.$get();
const body = await response.json(); // <-- this is typed as {msg: string, date: Date} <<< 

What do you see instead?

The Date type from the return of the handler is converted to a string. In how I see this, the client does not de-serialize the string from the network request back to the Date class, as it should.

Additional information

The same behaviour is seen from the testClient() function.

// in server test files
const client = testClient(app);
const response = await client.$get();
const body = await response.json(); // <-- this is typed as {msg: string, date: string}
                                    // It should be {msg: string, date: Date}
@askorupskyy
Copy link
Contributor

I think this is the correct behavior. The date is being passed as a string because JSON itself does not have a Date object, so instead it passes in the ISO 8601 format, which each language can then turn into a Date object. As for de-serializing we can’t really know whether the string we pass is really a date unless in runtime.

@yusukebe
Copy link
Member

@askorupskyy is right. This is not a bug.

@yusukebe yusukebe added not bug and removed triage labels Dec 26, 2024
@cybercoder-naj
Copy link
Author

True, I understand the reason why it is a string. However, the term RPC (Remote Procedure Call) should essentially, to the developer, be like calling a regular function, in this case, calling await client.$get(...) should return a Date regardless how it works behind the scenes.

In my understanding of RPC, not only the request payload is sent but also some optional metadata, which can be used to decipher how to serialize/de-serialize objects, which isnt universal to all languages/architectures.

@askorupskyy
Copy link
Contributor

askorupskyy commented Dec 27, 2024

@cybercoder-naj I agree, but tbh I don't think this should be a part of Hono's core functionality. Even tRPC does not have this out of the box.

Instead what Hono should aim for is the support for superjson – a more complex JSON serializer that allow for rgx, date, map, set (de)serialization. You can try to play around with this and build your own solution. Luckily, extensive Hono context API and custom fetch override should do it.

Here's a rough example of what this could look like:

import { Context } from 'hono';

const sendSerializedJson<P extends Record<string, unknown>>(c: Context, payload: P){
  return c.json(superjson.stringify(payload) as P); // should give you RPC type completion, haven't tested
}

app.get('/users', async (c) => {
  const users = await db.getUsers();
  return sendSerializedJson(c, { users });
})

export const client = hc<AppType>('localhost:whatever', {
  fetch: async (req: RequestInfo | URL, init?: RequestInit) => {
    const req = await fetch(req, init);
    const raw = await req.json();
    return superjson.deserialize(raw);
  }
})

Still, I'd advocate against building any serious software in such a manner for two reasons:

  • Performance – every field will be serialized & deserialized whether you want it or not. There's also stuff as network/browser limitations (while unlikely, but extra metadata will make the cap on your max JSON payload even smaller)
  • Scaling – this instantly limits your future stack around superjson or whatever validator/serializer you decide to use. If you need a Java client, for instance – you're screwed. This I think is the reason why Hono doesn't ship this out of the box.

Also, this instantly makes shipping any documentation a lot harder – most of API docs rely on OpenAPI, which with this extra metadata will be hard to maintain.

@cybercoder-naj
Copy link
Author

Understood, thanks for the detailed explanation.

@mamlzy
Copy link

mamlzy commented Jan 10, 2025

so @askorupskyy, from what you said, serializing date is a bad idea right? but i'm curious how you handle this, are you okay with date string on client?

@askorupskyy
Copy link
Contributor

askorupskyy commented Jan 10, 2025

@mamlzy having Date as a string is fine for me & I just delegate that to the client. Like I mentioned above, using complex serializers will most likely solve your issue but will bring many more as your software scales. Probably the most annoying thing I had to do with dates on the client is having to const dates = records.map(r => new Date(r.created_at)); when drawing a chart or something like that. There's also libraries such as moment.js to make it even easier.

Yes, it's annoying, but so much better than having to rewrite the entire project because a new service written in rust, for example, does not have a superjson library. JSON is a standard, (de)serialization is not.

@mamlzy
Copy link

mamlzy commented Jan 10, 2025

@mamlzy having Date as a string is fine for me & I just delegate that to the client. Like I mentioned above, using complex serializers will most likely solve your issue but will bring many more as your software scales. Probably the most annoying thing I had to do with dates on the client is having to const dates = records.map(r => new Date(r.created_at)); when drawing a chart or something like that. There's also libraries such as moment.js to make it even easier.

Yes, it's annoying, but so much better than having to rewrite the entire project because a new service written in rust, for example, does not have a superjson library. JSON is a standard, (de)serialization is not.

Got it! thanks for your insight!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants