router-for-me/CLIProxyAPI · error

target dynamic library must be at zip root

Error message

target dynamic library must be at zip root

What it means

Thrown by readTargetLibrary when a dynamic-library entry's base name matches the expected target (id+ext or id-v+version+ext) but the entry is nested inside a subdirectory rather than at the zip root (cleanedName != base). The installer locates the library at the archive root so the extracted path is deterministic and cannot smuggle path components; a nested placement with the right basename is treated as a layout error.

Source

Thrown at internal/pluginstore/install.go:338

	versionedTargetName := versionedPluginFileName(id, version, goos)
	var target *zip.File
	for _, file := range reader.File {
		cleanedName, errClean := cleanZipName(file.Name)
		if errClean != nil {
			return nil, 0, errClean
		}
		if file.FileInfo().IsDir() {
			continue
		}
		if !regularZipFile(file) {
			return nil, 0, fmt.Errorf("zip entry %s is not a regular file", file.Name)
		}
		if !hasDynamicLibraryExtension(cleanedName) {
			continue
		}
		if cleanedName != targetName && cleanedName != versionedTargetName {
			if path.Base(cleanedName) == targetName || path.Base(cleanedName) == versionedTargetName {
				return nil, 0, fmt.Errorf("target dynamic library must be at zip root")
			}
			return nil, 0, fmt.Errorf("dynamic library filename must be %s or %s", targetName, versionedTargetName)
		}
		if target != nil {
			return nil, 0, fmt.Errorf("zip contains multiple target dynamic libraries")
		}
		target = file
	}
	if target == nil {
		return nil, 0, fmt.Errorf("zip does not contain %s", targetName)
	}

	handle, errOpen := target.Open()
	if errOpen != nil {
		return nil, 0, fmt.Errorf("open %s: %w", targetName, errOpen)
	}
	defer func() {
		if errClose := handle.Close(); errClose != nil {

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Rebuild the zip so the library sits at the root: cd into the directory containing the .so/.dylib/.dll and zip from there
  2. Adjust the packaging script to flatten output before archiving (e.g. '(cd build/out && zip ../plugin.zip myplugin.so)')

Example fix

# before
zip plugin.zip build/linux/myplugin.so # entry: build/linux/myplugin.so

# after
cd build/linux && zip ../../plugin.zip myplugin.so # entry: myplugin.so (root)
Defensive patterns

Strategy: validation

Validate before calling

func libraryAtZipRoot(data []byte, id, version, goos string) error {
    zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
    if err != nil {
        return err
    }
    ext := ".so"
    switch goos {
    case "darwin":
        ext = ".dylib"
    case "windows":
        ext = ".dll"
    }
    plain := strings.TrimSpace(id) + ext
    versioned := strings.TrimSpace(id) + "-v" + strings.TrimPrefix(strings.TrimSpace(version), "v") + ext
    for _, f := range zr.File {
        if f.FileInfo().IsDir() || !strings.HasSuffix(f.Name, ext) {
            continue
        }
        if f.Name != path.Base(f.Name) && (path.Base(f.Name) == plain || path.Base(f.Name) == versioned) {
            return fmt.Errorf("library %s is nested at %s; must be at zip root", path.Base(f.Name), f.Name)
        }
    }
    return nil
}

Type guard

func isNestedLibraryError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "must be at zip root")
}

Try / catch

if _, err := pluginstore.InstallArchive(data, plugin, options); err != nil {
    if isNestedLibraryError(err) {
        // repackage: flatten the archive so the library sits at the root
    }
}

Prevention

When it happens

Trigger: Archive laid out as 'bin/myplugin.so' or 'dist/linux/amd64/myplugin-v1.0.0.so' — the basename matches but the full cleaned path is not the root. Any matching-extension entry in a subdirectory with the target basename triggers this.

Common situations: Build pipelines that zip a project directory (creating 'target/release/' or 'build/' prefixes); CI artifacts wrapping binaries in a top-level folder; repackaging a release tarball into a zip without flattening.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/2aa0c96e065232ed. Report an issue: GitHub.