# How to customize the URL reported back to the client in @tus/server

**URL:** <https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457>\
**Category:** Tus\
**Created:** [February 6, 2025, 8:04am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457 "2025-02-06T08:04:20Z")\
**Posts on this page:** 14\
**Page:** 1

<div class="post-metadata">

**Author:** ![empz](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/empz/32/1269_2.png) [@empz](https://community.transloadit.com/u/empz)\
**Post date:** [February 6, 2025, 8:04am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/1 "2025-02-06T08:04:20Z")

</div>

I have a small `@tus/node-server` with the `@tus/s3-store` to upload files to a Tigris bucket.

I’ve followed the documentation to specify a custom path on the storage based on the userId.

The files are properly being uploaded to the right location in my bucket.

I’m using Uppy (React) with the Tus uploader.

How can I build a URL for the uploaded file and read that in the `upload-success` event on the client? This event gets an `uploadURL` which is the URL of the specific Tus server request, not the actual file URL on the destination (the Tigris bucket).

```ts
import { Server } from '@tus/server'
import { S3Store } from '@tus/s3-store'
import { consola } from "consola";
import { nanoid } from "nanoid"
import invariant from 'tiny-invariant';

const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || 8080

invariant(process.env.S3_BUCKET, "S3_BUCKET is required")
invariant(process.env.S3_ACCESS_KEY_ID, "S3_ACCESS_KEY_ID is required")
invariant(process.env.S3_SECRET_ACCESS_KEY, "S3_SECRET_ACCESS_KEY is required")

const S3_ENDPOINT = process.env.S3_ENDPOINT || "https://fly.storage.tigris.dev"
const S3_BUCKET = process.env.S3_BUCKET

const server = new Server({
  path: '/files',
  datastore: new S3Store({
    s3ClientConfig: {
      endpoint: S3_ENDPOINT,
      region: "auto",
      bucket: S3_BUCKET,
      credentials: {
        accessKeyId: process.env.S3_ACCESS_KEY_ID,
        secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
      }
    }
  }),

  namingFunction(req, metadata) {
    invariant(metadata?.filename, "filename is required")
    invariant(metadata?.userId, "userId is required")

    const id = nanoid()
    const userId = metadata.userId;
    const filename = metadata.filename;

    const extension = filename.split('.').pop();

    return `${userId}/${id}.${extension}`
  },

  generateUrl(req, { proto, host, path, id }) {
    id = Buffer.from(id, 'utf-8').toString('base64url')

    return `${proto}://${host}${path}/${id}`
  },

  getFileIdFromRequest(req, lastPath) {
    // lastPath is everything after the last `/`
    // If your custom URL is different, this might be undefined
    // and you need to extract the ID yourself
    return Buffer.from(lastPath ?? "", 'base64url').toString('utf-8')
  },

  onUploadFinish: async (req, res, upload) => {
    consola.info(`Upload finished: ${upload.id}`)

    // Is this where I can build the file url? If so how?
    // I want to be able to build the file URL like:
    // const url = `${S3_ENDPOINT}/${S3_BUCKET}/${pathToFile}`
    // And get that in the `upload-success` (or any similar event) on Uppy.js

    return res
  }
})

consola.info(`Server is running on ${host}:${port}`)
server.listen({ host, port })

```

---

<div class="post-metadata">

**Author:** ![Merlijn](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/merlijn/32/1382_2.png) [@Merlijn](https://community.transloadit.com/u/Merlijn)\
**Post date:** [February 6, 2025, 8:14am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/2 "2025-02-06T08:14:57Z")

</div>

Hi, retrieving files is not part of the tus specification so you will have to write a [get handler](https://github.com/tus/tus-node-server/tree/main/packages/server#servergetreq-res) yourself.

I would be open to adding an example to the @tus/s3-store readme for getting files based on what you come up with.

---

<div class="post-metadata">

**Author:** ![empz](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/empz/32/1269_2.png) [@empz](https://community.transloadit.com/u/empz)\
**Post date:** [February 6, 2025, 8:16am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/3 "2025-02-06T08:16:46Z")

</div>

I think I might have not explained myself properly. I’m not looking to make the tus-server to retrieve the file. I just want to return the direct URL.

When using Transloadit, Uppy receives the file URL after the assembly is finished.

---

<div class="post-metadata">

**Author:** ![empz](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/empz/32/1269_2.png) [@empz](https://community.transloadit.com/u/empz)\
**Post date:** [February 6, 2025, 8:21am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/4 "2025-02-06T08:21:02Z")

</div>

Is my use case really that uncommon? I mean, you upload a file, then get a result, if it succeeded you get some information about the result. Among that information, it would make a lot of sense to have an URL to the file.  
Otherwise, I have to build that logic on the client and it doesn’t belong there. The responsible of knowing how the uploaded file is handled, where is it stored and so it’s the server. Why can’t it just return a URL back to the client after the upload is completed…?

---

<div class="post-metadata">

**Author:** ![Merlijn](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/merlijn/32/1382_2.png) [@Merlijn](https://community.transloadit.com/u/Merlijn)\
**Post date:** [February 6, 2025, 8:24am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/5 "2025-02-06T08:24:28Z")

</div>

You can use [`onUploadFinish`](https://github.com/tus/tus-node-server/tree/main/packages/server#optionsonuploadfinish) and return status 200 (otherwise body is not allowed) and the url in the body. Or don’t change the status code and use a header. Then you can get it somehow in `onAfterResponse` for `@uppy/tus`.

---

<div class="post-metadata">

**Author:** ![Merlijn](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/merlijn/32/1382_2.png) [@Merlijn](https://community.transloadit.com/u/Merlijn)\
**Post date:** [February 6, 2025, 8:28am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/6 "2025-02-06T08:28:31Z")

</div>

Actually you can just access it without `onAfterResponse`.

```js
uppy.on('upload-success', (file, res) => {
  console.log(res.body.xhr)
})

```

---

<div class="post-metadata">

**Author:** ![empz](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/empz/32/1269_2.png) [@empz](https://community.transloadit.com/u/empz)\
**Post date:** [February 6, 2025, 8:40am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/7 "2025-02-06T08:40:02Z")

</div>

Can you show a full example of it? This is what I’m doing on the server:

```ts
  onUploadFinish: async (req, res, upload) => {
    consola.info(`Upload finished: ${upload.id}`)

    return {
      res,
      status_code: 200,
      body: JSON.stringify({
        url: `https://${S3_ENDPOINT}/${S3_BUCKET}/${upload.id}`
      })
    }
  }

```

And on the client:

```ts
uppy.on("upload-success", (file, res) => {
  debugger;
  console.log("upload-success", res.body?.xhr);
})

```

But `res` doesn’t have the body set on the server.

 ![image](https://canada1.discourse-cdn.com/flex032/uploads/transloadit/original/2X/6/693657eced56a3ea0699b1c920fb67baec2c3007.png)

---

<div class="post-metadata">

**Author:** ![Merlijn](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/merlijn/32/1382_2.png) [@Merlijn](https://community.transloadit.com/u/Merlijn)\
**Post date:** [February 6, 2025, 9:44am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/8 "2025-02-06T09:44:54Z")

</div>

Do you see it returned in the network tab? If so, can you access it in with `onAfterResponse` for `@uppy/tus`? If you can’t access it be do see it please create an uppy issue. If you don’t see it in the network tab create an issue for tus-node-server.

---

<div class="post-metadata">

**Author:** ![empz](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/empz/32/1269_2.png) [@empz](https://community.transloadit.com/u/empz)\
**Post date:** [February 6, 2025, 9:48am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/9 "2025-02-06T09:48:50Z")

</div>

It’s not on the network tab either.

The response is 200 but with an empty body no matter what I return in the `onUploadFinish` method.

```auto
HTTP/1.1 200 OK
Tus-Resumable: 1.0.0
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Expose-Headers: Authorization, Content-Type, Location, Tus-Extension, Tus-Max-Size, Tus-Resumable, Tus-Version, Upload-Concat, Upload-Defer-Length, Upload-Length, Upload-Metadata, Upload-Offset, X-HTTP-Method-Override, X-Requested-With, X-Forwarded-Host, X-Forwarded-Proto, Forwarded
Cache-Control: no-store
Upload-Offset: 4483969
Upload-Length: 4483969
Upload-Metadata: relativePath bnVsbA==,name aXRzLW15LWxpZmUubXAz,type YXVkaW8vbXBlZw==,userId M2E0ZjlkNGUtZTg4Mi00MzI4LTg2NDUtYmQyOTNiZTAxNDIy,contentType YXVkaW8vbXBlZw==,filetype YXVkaW8vbXBlZw==,filename aXRzLW15LWxpZmUubXAz
Date: Thu, 06 Feb 2025 08:45:17 GMT
Connection: keep-alive
Keep-Alive: timeout=5

```

---

<div class="post-metadata">

**Author:** ![empz](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/empz/32/1269_2.png) [@empz](https://community.transloadit.com/u/empz)\
**Post date:** [February 6, 2025, 9:52am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/10 "2025-02-06T09:52:41Z")

</div>

> <https://github.com/tus/tus-node-server/issues/714>
>
> \### Initial checklist
> 
> \- \[x\] I understand this is a bug report and questions sho…uld be posted in the \[Community Forum\](https://community.transloadit.com/c/tus/6)
> \- \[x\] I searched \[issues\](https://github.com/tus/tus-node-server/issues?q=is%3Aissue) and couldn’t find anything (or linked relevant results below)
> 
> \### Steps to reproduce
> 
> \`\`\`ts
> onUploadFinish: async (req, res, upload) =\> {
> return {
> res,
> status\_code: 204,
> body: \`https://${S3\_ENDPOINT}/${S3\_BUCKET}/${upload.id}\`
> }
> }
> \`\`\`
> 
> Context: https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/7
> 
> \### Expected behavior
> 
> The response contains the right 204 status code and body.
> 
> \### Actual behavior
> 
> No matter what's returned on the \`onUploadFinish\`, the response is always 200 with an empty body.
> 
> Example response with above code. Should be 204 and body should not be empty.
> \`\`\`
> HTTP/1.1 200 OK
> Tus-Resumable: 1.0.0
> Access-Control-Allow-Origin: http://localhost:3000
> Access-Control-Expose-Headers: Authorization, Content-Type, Location, Tus-Extension, Tus-Max-Size, Tus-Resumable, Tus-Version, Upload-Concat, Upload-Defer-Length, Upload-Length, Upload-Metadata, Upload-Offset, X-HTTP-Method-Override, X-Requested-With, X-Forwarded-Host, X-Forwarded-Proto, Forwarded
> Cache-Control: no-store
> Upload-Offset: 4483969
> Upload-Length: 4483969
> Upload-Metadata: relativePath bnVsbA==,name aXRzLW15LWxpZmUubXAz,type YXVkaW8vbXBlZw==,userId M2E0ZjlkNGUtZTg4Mi00MzI4LTg2NDUtYmQyOTNiZTAxNDIy,contentType YXVkaW8vbXBlZw==,filetype YXVkaW8vbXBlZw==,filename aXRzLW15LWxpZmUubXAz
> Date: Thu, 06 Feb 2025 08:45:17 GMT
> Connection: keep-alive
> Keep-Alive: timeout=5
> 
> \`\`\`

---

<div class="post-metadata">

**Author:** ![empz](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/empz/32/1269_2.png) [@empz](https://community.transloadit.com/u/empz)\
**Post date:** [February 6, 2025, 12:03pm UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/11 "2025-02-06T12:03:55Z")

</div>

Ok, so the thing is that the server seems to be skipping/ignoring already uploaded files, but it’s returning a 200 empty response back to the client.

How can I disable this cache or ignore thing? I want the server to process each file upload independently as a new file, even though the file uploaded might be the same one over and over again.

---

<div class="post-metadata">

**Author:** ![marius](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/marius/32/572_2.png) [@marius](https://community.transloadit.com/u/marius)\
**Post date:** [February 7, 2025, 9:29am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/12 "2025-02-07T09:29:27Z")

</div>

It’s not the server ignoring files, but the client. It recognizes that it uploaded the same file and has to ability to resume the previous upload which only consists of a single request to check if the server still has the file.

If you don’t want this behavior, configure the client to not resume completed uploads.

---

<div class="post-metadata">

**Author:** ![empz](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/empz/32/1269_2.png) [@empz](https://community.transloadit.com/u/empz)\
**Post date:** [February 7, 2025, 5:38pm UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/13 "2025-02-07T17:38:35Z")

</div>

Ok, but how exactly does the “single request to check if the server still has the file” works? Is there some event I can listen to?

If the server still has the file, how can I modify the response back to the client to include a URL to the file just like I do in the `onUploadFinish` event? I’m simply trying to have a consistent response to the client in both cases: “new file uploaded” and “file was previously uploaded, returning existing URL”.

---

<div class="post-metadata">

**Author:** ![marius](https://yyz2.discourse-cdn.com/flex032/user_avatar/community.transloadit.com/marius/32/572_2.png) [@marius](https://community.transloadit.com/u/marius)\
**Post date:** [February 12, 2025, 9:07am UTC](https://community.transloadit.com/t/how-to-customize-the-url-reported-back-to-the-client-in-tus-server/17457/14 "2025-02-12T09:07:06Z")

</div>

> [@empz](#):
>
> Ok, but how exactly does the “single request to check if the server still has the file” works? Is there some event I can listen to?

Tus-js-client sends a HEAD request (see [Resumable upload protocol 1.0.x | tus.io](https://tus.io/protocols/resumable-upload)) to check the upload’s status. On the server-side, there are no events so far to modify the server’s response.

> [@empz](#):
>
> I’m simply trying to have a consistent response to the client in both cases: “new file uploaded” and “file was previously uploaded, returning existing URL”.

That’s currently not possible with tus-node-server. @Merlijn and I have been brainstorming about how this situation can be improved using metadata. The server could attach metadata once the upload is finished, which the client can then retrieve when either the upload is finished or the client notices that the upload is already done.
