Forge can't create Asset objects - why?

This code snippet being called in a useEffect:

(given a valid workspaceId and request body)

  const response = await requestJira(`/jsm/assets/workspace/${workspaceId}/v1/object/create`, {
    method: 'POST', 
    headers: {
        'Content-Type': 'application/json',  // This is crucial for POST requests with a JSON body
    },
    body: JSON.stringify(body),
  });
  if (!response.ok) {
      throw new Error(`Failed to create object: ${response.status} ${response.statusText}`);
  }

always gives 400 Bad Request with a basic HTML document. No further information is given as to why it’s a bad request, unusual for the Assets API.

If I copy the request as ā€˜cUrl (bash)’ from my browser’s devtools network window then follow these steps:

  1. Delete ā€œex/jira/{cloud site UUID}ā€ from the request URL
  2. Delete the ā€˜authorisation’ header
  3. Authorise with my account email and API token, ie. ā€˜-u $FORGE_EMAIL:$FORGE_API_TOKEN’

Then when I execute that cUrl command, I get 201 Created and the expected API response.

Why can’t Forge create Asset objects?

Hi Alex,

the 400 error code is indeed strange. And since you’re able to create an object with the same payload, you’re using the REST API correctly.

Could you share the scopes used in your manifest?

Cheers,
Jonathan

Sure thing! I’ll tighten these up as the product development progresses, but here’s what I’m working with now:

  scopes:
    - 'read:jira-user'
    - 'read:jira-work'
    - 'write:jira-work'
    - 'manage:jira-project'
    - 'manage:jira-configuration'
    - 'manage:jira-webhook'
    - 'read:servicedesk-request'
    - 'write:servicedesk-request'
    - 'manage:servicedesk-customer'
    - 'read:app-system-token'
    - 'read:app-user-token'
    - 'storage:app'
    - 'report:personal-data'
    - 'import:import-configuration:cmdb'
    - 'write:cmdb-object:jira'
    - 'delete:cmdb-object:jira'
    - 'read:cmdb-object:jira'
    - 'write:cmdb-schema:jira'
    - 'delete:cmdb-schema:jira'
    - 'read:cmdb-schema:jira'
    - 'write:cmdb-type:jira'
    - 'delete:cmdb-type:jira'
    - 'read:cmdb-type:jira'
    - 'write:cmdb-attribute:jira'
    - 'delete:cmdb-attribute:jira'
    - 'read:cmdb-attribute:jira'
    - 'read:cmdb-icon:jira'

I tried this in a Forge sandbox app with just these scopes:

- write:cmdb-object:jira
- read:servicedesk-request

The app just gets the workspaceId then creates an object, and this time it was 201 Created.

It seems then that the issue has to do with the amount of Oauth scopes in my development app. I’ll likely find it works after I cut out the more obviously extraneous ones.

I appreciate the assistance!

1 Like

Thanks for providing this update!
I’ll follow up on this on our end as using fine granular scopes shouldn’t cause a 400 – or at the very least there should be a developer-friendly error message if there’s a know limitation.

Reducing to these scopes has ended up in a curious state:

    - 'read:jira-user'
    - 'read:jira-work'
    - 'write:jira-work'
    - 'manage:jira-project'
    - 'manage:jira-configuration'
    - 'manage:jira-webhook'
    - 'read:servicedesk-request'
    - 'write:servicedesk-request'
    - 'manage:servicedesk-customer'
    - 'read:app-system-token'
    - 'read:app-user-token'
    - 'storage:app'
    - 'report:personal-data'
    - 'write:cmdb-object:jira'
    - 'read:cmdb-object:jira'
    - 'read:cmdb-schema:jira'
    - 'read:cmdb-type:jira'
    - 'read:cmdb-attribute:jira'
    - 'read:cmdb-icon:jira'

The frontend, that is, client fetch action gives 400 Bad Request, but the backend, that is, invoke resolver action gives 201. That’s better than previous, where the backend would also give 400 Bad Request.

Here is the TSX component and resolver function that demonstrates this.

const body = {
  "objectTypeId": "91", // Valid object type ID
  "attributes": [{
      "objectTypeAttributeId": "877", // Attribute id of the "Name"
      "objectAttributeValues": [{ "value": "Object created"}]
  }]}

const BasicTest = () => {

  const [workspaceId, setWorkspaceId] = useState<string | null>(null);
  const [backendResult, setBackendResult] = useState<string | null>(null);
  const [frontendResult, setFrontendResult] = useState<string | null>(null);

  useEffect(() => {
    requestJira('/rest/servicedeskapi/assets/workspace').then((response) => 
      response.json().then((response) => setWorkspaceId(response.values[0].workspaceId)));
  }, []);

  const createObject = (formdata: { [k: string]: any }) => {
    try {
      requestJira(`/jsm/assets/workspace/${workspaceId}/v1/object/create`,
        { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: formdata['json'] }
      ).then((response) => response.text().then((text) =>
        setFrontendResult(`${response.status} ${response.statusText}\n${text}`))
      );
    } catch (e: any) { setFrontendResult(String(e.message ? e.message : e)) };

    try {
      invoke('createObject', { body: formdata['json'], workspaceId: workspaceId }).then((response) =>
        setBackendResult(response as string));
    } catch (e: any) { setBackendResult(String(e.message ? e.message : e)) };

  }

  const { register, handleSubmit, getFieldId } = useForm({ defaultValues: { json: JSON.stringify(body, null, 2) } });

  return (<Box>
    <Form onSubmit={handleSubmit((formdata: { [k: string]: any }) => createObject(formdata))}>
      <Label labelFor={getFieldId('json')}>Create Object JSON</Label>
      <TextArea {...register('json')} isMonospaced={true} />
      <Button appearance='primary' type="submit">Create Object</Button>
    </Form>
    {frontendResult && <><Label labelFor="frontend">Frontend</Label><Code>{frontendResult}</Code></>}
    {backendResult && <><Label labelFor="backend">Backend</Label><Code>{backendResult}</Code></>}
  </Box>);
}
resolver.define('createObject', async (req): Promise<string> => {
  const workspaceId = req.payload['workspaceId'];
  try {
    const response = await api.asApp().requestJira(route`/jsm/assets/workspace/${workspaceId}/v1/object/create`, 
      { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: req.payload['body'] });
    const text = await response.text();
    return `${response.status} ${response.statusText}\n${text}`;
  } catch (e: any) { return String(e.message ? e.message : e) }
});