Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions v2/app/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,10 @@ var (
"Conflicting service definitions",
"A service cannot use both an //encore:service directive and a service.New() call.",
)

errExposeUnknownGateway = errRange.Newf(
"Endpoint exposed on undeclared gateway",
"The endpoint is exposed on gateway %q via //encore:expose, but no gateway with that name has been declared.",
errors.WithDetails("Add gateway.New(\"<name>\", gateway.Config{}) in an encore.gateway.go file. The implicit \"api-gateway\" name is only available when no gateways are declared at all."),
)
)
50 changes: 50 additions & 0 deletions v2/app/testdata/expose_unknown_gateway.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Verify that //encore:expose with a gateway name that hasn't been declared
# (and isn't the synthesized "api-gateway" default) is rejected.
! parse
err 'exposed on undeclared gateway'

-- encore.app --
{"edition": "2026"}

-- gw/encore.gateway.go --
package gw

import "encore.dev/gateway"

var _ = gateway.New("api", gateway.Config{})

-- svc/encore.service.go --
package svc

import "encore.dev/service"

type MyService struct{}

var _ = service.New("svc", service.Config[MyService]{})

-- svc/svc.go --
package svc

import "context"

//encore:api path=/hello
//encore:expose gateway=does-not-exist
func Hello(ctx context.Context) error { return nil }
-- want: errors --

── Endpoint exposed on undeclared gateway ─────────────────────────────────────────────────[E9999]──

The endpoint is exposed on gateway "does-not-exist" via //encore:expose, but no gateway with that
name has been declared.

╭─[ svc/svc.go:6:17 ]
4 │
5 │ //encore:api path=/hello
6 │ //encore:expose gateway=does-not-exist
⋮ ──────────────────────
7 │ func Hello(ctx context.Context) error { return nil }
───╯

Add gateway.New("<name>", gateway.Config{}) in an encore.gateway.go file. The implicit
"api-gateway" name is only available when no gateways are declared at all.
32 changes: 32 additions & 0 deletions v2/app/testdata/expose_user_named_gateway.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Verify that a user-declared gateway with a non-default name works end-to-end:
# the endpoint's expose list should reference the user's gateway, and the
# implicit "api-gateway" should NOT be synthesized when any gateway is declared.
parse

-- encore.app --
{"edition": "2026"}

-- gw/encore.gateway.go --
package gw

import "encore.dev/gateway"

var _ = gateway.New("api", gateway.Config{})

-- svc/encore.service.go --
package svc

import "encore.dev/service"

type MyService struct{}

var _ = service.New("svc", service.Config[MyService]{})

-- svc/svc.go --
package svc

import "context"

//encore:api path=/hello
//encore:expose gateway=api
func Hello(ctx context.Context) error { return nil }
10 changes: 1 addition & 9 deletions v2/app/v2meta/v2meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (

metav2 "encore.dev/appruntime/exported/proto/encore/parser/meta/v2"
schemapb "encore.dev/appruntime/exported/proto/encore/parser/schema/v2"
"encr.dev/pkg/fns"
sourcev1 "encore.dev/appruntime/exported/proto/encore/parser/source/v1"
"encr.dev/pkg/fns"
"encr.dev/pkg/idents"
"encr.dev/pkg/option"
"encr.dev/v2/app"
Expand Down Expand Up @@ -543,13 +543,6 @@ func (b *builder) build() {
}
}

// Ensure a default gateway exists.
if len(b.res.Gateways) == 0 {
b.res.Gateways["api-gateway"] = &metav2.Gateway{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be emitted in the metadata?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, we should, but we insert it in two different places, so I just removed this one.

We add it in discoverGateways here:

return []*Gateway{{EncoreName: "api-gateway"}}

RepoId: "default",
Name: "api-gateway",
}
}
// For legacy edition (single auth handler), attach it to the first gateway.
// For edition 2026 with multiple handlers, gateways get their handler from
// the gateway.New() config (already parsed). For now, attach the first handler
Expand Down Expand Up @@ -1321,7 +1314,6 @@ func (b *builder) secrets(r *secrets.Secrets) {
}
}


func (b *builder) apiPath(path *resourcepaths.Path) *metav2.Path {
res := &metav2.Path{}
for _, p := range path.Segments {
Expand Down
18 changes: 18 additions & 0 deletions v2/app/validate_apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ func (d *Desc) validateAPIs(pc *parsectx.Context, fw *apiframework.AppDesc, resu

apiPaths := resourcepaths.NewSet()

// Build a set of declared gateway names. discoverGateways guarantees
// "api-gateway" is included when no gateway has been explicitly declared.
declaredGateways := make(map[string]struct{}, len(d.Gateways))
for _, gw := range d.Gateways {
declaredGateways[gw.EncoreName] = struct{}{}
}

for _, svc := range d.Services {
fwSvc, ok := svc.Framework.Get()
if !ok {
Expand All @@ -43,6 +50,17 @@ func (d *Desc) validateAPIs(pc *parsectx.Context, fw *apiframework.AppDesc, resu
)
}

// Verify each gateway named in //encore:expose has been declared
// (or matches the synthesized default). Only edition 2026 endpoints
// can populate ExposedGateways; legacy endpoints leave it empty.
for _, name := range ep.ExposedGateways {
if _, ok := declaredGateways[name]; ok {
continue
}
err := errExposeUnknownGateway(name)
pc.Errs.Add(errors.AtOptionalNode(err, ep.ExposeField))
}

// Check for duplicate paths by adding them to the set
// Note, errors will be reported automatically to pc.Errs
for _, method := range ep.HTTPMethods {
Expand Down
27 changes: 19 additions & 8 deletions v2/codegenv2/servergen/servergen.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

"github.com/dave/jennifer/jen"

metav2 "encore.dev/appruntime/exported/proto/encore/parser/meta/v2"
schemapb "encore.dev/appruntime/exported/proto/encore/parser/schema/v2"
"encr.dev/pkg/paths"
"encr.dev/v2/app"
"encr.dev/v2/app/apiframework"
Expand All @@ -20,8 +22,6 @@ import (
"encr.dev/v2/parser/apis/api"
"encr.dev/v2/parser/apis/selector"
"encr.dev/v2/parser/infra/pubsub"
metav2 "encore.dev/appruntime/exported/proto/encore/parser/meta/v2"
schemapb "encore.dev/appruntime/exported/proto/encore/parser/schema/v2"
)

const (
Expand Down Expand Up @@ -220,12 +220,23 @@ func genEndpoint(grpcService string, ep *api.Endpoint, handlerDecl *codegenv2.Va

// Set access-related fields.
switch ep.Access {
case api.Public:
d[jen.Id("NoAuthRequired")] = jen.True()
d[jen.Id("ExposedGateways")] = jen.Index().String().Values(jen.Lit("api-gateway"))
case api.Auth:
d[jen.Id("NoAuthRequired")] = jen.False()
d[jen.Id("ExposedGateways")] = jen.Index().String().Values(jen.Lit("api-gateway"))
case api.Public, api.Auth:
d[jen.Id("NoAuthRequired")] = jen.Lit(ep.Access == api.Public)

// Use the parser's expose list if populated (edition 2026 with
// //encore:expose gateway=...). Fall back to the synthesized
// "api-gateway" — discoverGateways guarantees a gateway with that
// name exists when no other gateway has been declared.
names := ep.ExposedGateways
if len(names) == 0 {
names = []string{"api-gateway"}
}
lits := make([]jen.Code, len(names))
for i, n := range names {
lits[i] = jen.Lit(n)
}
d[jen.Id("ExposedGateways")] = jen.Index().String().Values(lits...)

case api.Private:
d[jen.Id("NoAuthRequired")] = jen.True()
}
Expand Down