thingsboard/thingsboard · warning · ThingsboardException

BAD_REQUEST_PARAMS

BAD_REQUEST_PARAMS

Error message

The device must be a gateway!

What it means

The gateway-launch docker-compose endpoint builds a compose file that provisions the target device as an IoT gateway. Only devices flagged as gateways (additionalInfo.gateway = true) are valid targets, so any non-gateway device id is rejected with BAD_REQUEST_PARAMS before the compose file is generated.

Source

Thrown at application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java:159

                    @ApiResponse(
                            responseCode = "200",
                            description = "OK",
                            content = @Content(
                                    mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
                                    schema = @Schema(type = "string", format = "binary")
                            )
                    )
            })
    @RequestMapping(value = "/device-connectivity/gateway-launch/{deviceId}/docker-compose/download", method = RequestMethod.GET)
    @ResponseBody
    public ResponseEntity<org.springframework.core.io.Resource> downloadGatewayDockerCompose(@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
                                                                                             @PathVariable(DEVICE_ID) String strDeviceId, HttpServletRequest request) throws ThingsboardException, URISyntaxException, IOException {
        checkParameter(DEVICE_ID, strDeviceId);
        DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
        Device device = checkDeviceId(deviceId, Operation.READ_CREDENTIALS);

        if (!checkIsGateway(device)) {
            throw new ThingsboardException("The device must be a gateway!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
        }

        String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
        var dockerCompose = checkNotNull(deviceConnectivityService.createGatewayDockerComposeFile(baseUrl, device), "Failed to create docker-compose.yml file!");

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + DOCKER_COMPOSE_YML)
                .header("x-filename", DOCKER_COMPOSE_YML)
                .contentLength(dockerCompose.contentLength())
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(dockerCompose);
    }

    private static boolean checkIsGateway(Device device) {
        return device.getAdditionalInfo().has(DataConstants.GATEWAY_PARAMETER) &&
                device.getAdditionalInfo().get(DataConstants.GATEWAY_PARAMETER).asBoolean();
    }
}

View on GitHub (pinned to 45c30e83fa)

Solutions

  1. Pick a device that is actually a gateway (device.additionalInfo.gateway == true).
  2. Convert the device: call saveDevice with additionalInfo {"gateway": true}.
  3. If a new gateway is needed, create it via POST /api/device with the gateway flag set, then retry the download.

Example fix

// before
Device device = new Device();
device.setName("sensor-1"); // no gateway flag -> download fails

// after
Device device = new Device();
device.setName("gw-1");
device.setAdditionalInfo(JacksonUtil.newObjectNode().put("gateway", true));
Defensive patterns

Strategy: type-guard

Validate before calling

Device device = deviceClient.getDeviceById(deviceId);
boolean gateway = device.getAdditionalInfo() != null && device.getAdditionalInfo().has("gateway") && device.getAdditionalInfo().get("gateway").asBoolean();

Type guard

boolean isGatewayDevice(Device d) {
    return d.getAdditionalInfo() != null
        && d.getAdditionalInfo().path("gateway").asBoolean(false);
}

Try / catch

try {
    return connectivityClient.downloadGatewayDockerCompose(deviceId);
} catch (ThingsboardException e) {
    if ("The device must be a gateway!".equals(e.getMessage())) {
        return promptUserToCreateGateway(); // or set the flag then retry
    }
    throw e;
}

Prevention

When it happens

Trigger: GET /api/device-connectivity/gateway-launch/{deviceId}/docker-compose/download where the device was created without the 'Is gateway' flag — typically the user picked the wrong device or created it via API without additionalInfo.gateway=true.

Common situations: Provisioning a regular device and then trying to fetch gateway launch instructions for it; device created via REST/mqtt claim without the gateway flag; UI dropdown listing all devices instead of only gateways.

Related errors


AI-assisted analysis of thingsboard/thingsboard@45c30e83fa (2026-08-14). Data as JSON: /api/errors/4eab7c8746ef60a8. Report an issue: GitHub.